try/catch Is a Safety Net, Not a Seatbelt
When a line of JavaScript throws, execution stops dead and the error propagates up the call stack. If nothing catches it, the program crashes (in Node) or logs a red wall in the console (in browsers). try/catch is how you intercept that — say "I know this might fail, here's what I want to do instead."
The basic shape:
JSON.parse throws a SyntaxError. Execution jumps immediately to the catch block with the error bound to err. The third console.log still runs — the crash was contained.
If the try block succeeds without throwing, the catch block is skipped entirely. It's there for the failure path.
The Error Object
Whatever is thrown gets bound to the parameter name in catch (...). Usually it's an Error instance with three useful fields:
name tells you the subclass (TypeError, RangeError, SyntaxError, etc. — more on those in the next doc). message is the human-readable description. stack is the full trace, which is gold when debugging.
One gotcha: JavaScript lets you throw anything, not just Error objects. Old code sometimes does throw "something broke". When you write your own throw, always throw an Error so callers get a stack trace:
finally Runs Either Way
finally is an optional third block that runs whether or not an error was thrown, and whether or not the catch handled it. It's for cleanup — closing files, releasing locks, hiding loading spinners:
The spinner gets hidden whether the load succeeded or failed. Without finally, you'd have to write that line in both branches — and forget it in one of them.
finally even runs if the try or catch block contains a return. The function returns after finally executes. That's occasionally surprising; mostly it's exactly what you want.
You Don't Always Need catch
catch is optional. A bare try/finally is legal and useful when you want guaranteed cleanup but have no intention of handling the error — you want it to propagate:
The inner try/finally releases the lock even when fn() throws, but doesn't swallow the error — the caller still sees it. Swallowing errors silently ("it failed and I didn't tell anyone") is one of the most common debugging nightmares.
Rethrowing: Handle Some, Pass Others Up
A catch block doesn't have to handle everything. You can inspect the error, handle what makes sense, and rethrow the rest:
The instanceof check is the pattern: recognize the errors you know how to recover from, and let anything else continue up the stack. Swallowing every error with an empty catch block is a code smell — you lose all signal when something unexpected goes wrong.
try/catch With async/await
Inside an async function, awaited promises that reject turn into thrown errors — and try/catch handles them the same way as synchronous errors:
One subtlety: you must await the promise inside the try block. If you return a promise without awaiting it, the rejection happens after the function has already exited, and catch never sees it:
async function bad() {
try {
return fetch("/broken"); // no await — caller sees the rejection
} catch (err) {
// never runs
}
}
Rule of thumb: in async functions, await the thing you want try/catch to cover.
Nested try/catch
You can nest try/catch blocks when inner and outer code fail for different reasons you want to handle differently:
The inner catch handles "data shape is wrong" by returning a safe default. The outer one handles "input wasn't JSON at all" by wrapping and rethrowing. Nesting is fine when each layer has a distinct recovery strategy — if both blocks would do the same thing, flatten them.
When Not to Use try/catch
try/catch is a tool for expected, recoverable failures. It's not a way to paper over bugs.
- Don't wrap a whole function body "just in case." If you have no real plan for the error, let it bubble up — an uncaught error with a stack trace is more useful than a silent one.
- Don't use it for control flow.
tryblocks have real overhead and muddy the code compared to anifcheck.if (user)beatstry { user.name } catch {}. - Don't catch and log-and-ignore. At minimum, rethrow or return a sentinel value your caller can detect.
The mental test: "what does the user of this code do when this fails?" If you don't have an answer, you're not ready to catch the error yet.
Quick Reference
try { ... } catch (err) { ... }— intercept thrown errors.finally { ... }— always runs; use for cleanup.throw new Error("...")— always throwErrorsubclasses so stack traces work.throw err;insidecatch— rethrow when you can't handle it.awaitinsidetry— required fortry/catchto see async rejections.
Next: Error Types
TypeError, RangeError, SyntaxError — JavaScript has a family of built-in error classes, and knowing which one means what makes catching and reporting much more precise. That's the next doc.
Frequently Asked Questions
How does try/catch work in JavaScript?
Put risky code inside try { ... }. If anything inside throws, execution jumps straight to the catch (err) { ... } block with the thrown value bound to err. If nothing throws, the catch block is skipped. An optional finally { ... } runs either way — useful for cleanup.
When should I use try/catch in JavaScript?
Use it around operations that can realistically fail at runtime: JSON.parse on untrusted input, fetch responses, file or network I/O. Don't wrap every line — if you have no plan for recovering, let the error bubble up. Broad try/catch around working code hides bugs instead of handling them.
Does try/catch catch async errors?
Only when you await the promise inside the try block. A bare somePromise() call won't be caught — the error becomes an unhandled rejection. With async/await, try/catch works the same as with synchronous code. For raw promises, use .catch() on the chain instead.
How do I rethrow an error in JavaScript?
Inside catch, just throw err; (or throw a new error that wraps it). This is useful when you want to handle some errors and let others propagate — check the error's type or message first, handle what you can, and rethrow the rest so callers upstream still see them.