try / catch / finally
The try statement is JavaScript's structured way to handle exceptions. You put risky code in a try block, intercept failures in a matching catch, and put cleanup that always runs in finally. The shape is small, but the rules around scope, error propagation and return inside finally have a few subtleties worth knowing.
The syntax
A try must be followed by at least one of catch or finally — and may have both. The catch binding is optional since ES2019.
try {
// code that may throw
} catch (err) {
// handle the error
} finally {
// always runs
}
// Optional catch binding (ES2019)
try {
JSON.parse(input);
} catch {
return null;
}What gets caught
A catch block runs when a synchronous throw happens inside the matching try. Asynchronous errors do not propagate to a surrounding try unless you await the promise.
try {
setTimeout(() => { throw new Error("late"); }, 0);
} catch (err) {
console.log("never runs — the error is thrown on a different turn");
}
try {
await Promise.reject(new Error("awaited"));
} catch (err) {
console.log("caught:", err.message); // "awaited"
}Scope of the catch binding
The variable in catch (err) is scoped to the catch block — like a let. It does not leak out, and you cannot redeclare another err in the same block.
function safeJson(s) {
let result;
try {
result = JSON.parse(s);
} catch (err) {
console.log("parse failed:", err.message);
result = null;
}
// `err` is out of scope here
return result;
}finally runs no matter what
The finally block runs after the try (and any matching catch) finishes, regardless of whether the block returned, threw, or completed normally. It is the right place for cleanup — closing files, releasing locks, restoring UI state.
function withLock(lock, fn) {
lock.acquire();
try {
return fn();
} finally {
lock.release(); // runs on normal return, on throw, on early break
}
}Even an uncaught error in try runs finally before propagating up the stack.
return and throw inside finally
Two surprising rules:
If
finallyreturns a value, it overrides any return fromtryorcatch.If
finallythrows, the new error replaces any pending error fromtryorcatch.
function weird() {
try {
return "from try";
} finally {
return "from finally"; // wins — the try return is discarded
}
}
console.log(weird()); // "from finally"from finally
Re-throwing and conditional catch
JavaScript has no typed catch (err: HttpError) syntax. Use an instanceof check inside the catch block and re-throw anything you do not handle.
try {
await loadConfig();
} catch (err) {
if (err instanceof SyntaxError) {
console.log("config file is malformed");
return defaults;
}
throw err; // every other error keeps propagating
}Nested try blocks
You can nest try statements. The innermost matching catch handles a thrown error; if it re-throws, the outer catch gets a chance.
try {
try {
risky();
} catch (inner) {
console.log("logging:", inner.message);
throw inner; // re-throw so the outer handler can react
} finally {
console.log("cleanup");
}
} catch (outer) {
console.log("recovered from", outer.message);
}Errors that escape
An error thrown inside a try and not caught by its catch (because there is no catch, or the catch re-throws) bubbles up the call stack. At the top of an async function this becomes a promise rejection; at the top of a sync stack the engine logs an unhandled error.
function fail() {
try {
throw new Error("from inside");
} finally {
console.log("finally still runs");
}
}
try {
fail();
} catch (err) {
console.log("caught at the outer level:", err.message);
}finally still runs caught at the outer level: from inside