JAVASCRIPT / ERRORS, STRICT MODE, AND DEBUGGING
try, catch, and finally for recovery
Recover from runtime failures by scoping try narrowly, returning fallbacks or rethrowing in catch, and guaranteeing cleanup in finally on every exit path.
What you will learn
- Scope try to the risky call only, not the whole function body
- Use catch to supply a fallback, retry, or rethrow what you cannot handle
- Put cleanup in finally so it runs on return, throw, and break alike
- Never return from finally: it replaces the try's result and erases errors
Understanding try, catch, and finally for recovery
A throw does not pause a line so you can repair it; it abandons the rest of the try block on the spot. The engine looks for the nearest enclosing catch, binds the thrown value to its parameter, runs that block, and then continues after the whole try statement as if the failure had been dealt with. That is why the line after a failing JSON.parse never runs, and why a try block should cover only the risky call plus the lines that depend on it. Everything else you put inside it is code whose bugs you have quietly volunteered to absorb.
catch is where recovery is decided, and there are three honest choices: substitute a value you can live with, attempt the operation again, or rethrow. The reason to check the error first, with something like err instanceof SyntaxError, is that catch is indiscriminate: it grabs your own typos and TypeErrors just as eagerly as the parse failure you were expecting. Recovering from a failure you never anticipated means the program keeps running on invented data, which moves the visible symptom far away from the real cause.
finally is not simply the code that comes after error handling; it is the block the engine promises to run on every way out of the try, whether that is falling off the end, a return, a break, or a throw still travelling upward. That guarantee is what makes it the right home for cleanup such as closing a handle, clearing a loading flag, or releasing a lock. It also explains the sharpest edge in this topic: return evaluates its expression, stores the result as a pending completion, then hands control to finally before actually leaving the function, so a return or throw inside finally overwrites that pending completion and can discard an error nobody ever hears about.
Because the recovery path is a separate branch, test it deliberately. Feed the function input you know breaks it and watch the log order; the ordering itself tells you whether finally really is running before the value reaches the caller.
function loadRetries(text) {
try {
const config = JSON.parse(text);
return config.retries;
} catch (err) {
console.log("bad config:", err.name);
return 3;
} finally {
console.log("done reading config");
}
}
console.log("retries:", loadRetries('{"retries": 5}'));
console.log("retries:", loadRetries('{"retries": 5'));A throw jumps out of the try block immediately, so catch decides the fallback while finally is the only block guaranteed to run on every exit path.
Worked examples
finally overrides the try block
Shows that a return inside finally discards a pending exception, while a finally that only cleans up lets the error through.
function swallow() {
try {
throw new Error("disk full");
} finally {
return "everything is fine";
}
}
function honest() {
try {
throw new Error("disk full");
} finally {
console.log("closing file");
}
}
console.log(swallow());
try {
honest();
} catch (err) {
console.log("caller saw:", err.message);
}Example explained
Line 1try/finally with no catch is legal: the error still propagates, finally just runs on the way out.
Line 2The return in swallow's finally replaces the pending throw, so the disk error is gone and the caller is told it succeeded.
Line 3honest only logs in finally, so the completion stays a throw and the outer catch receives the original Error.
Line 4The order closing file then caller saw proves finally runs before the exception reaches the handler.
Handle what you know, rethrow the rest
Filters the caught value so only the anticipated failure is recovered from and anything unexpected reaches the caller.
function readSetting(store, key) {
try {
return store.get(key);
} catch (err) {
if (err instanceof TypeError) {
console.log("no store, using default");
return "dark";
}
throw err;
}
}
const broken = {
get() {
throw new RangeError("shard offline");
}
};
console.log(readSetting({}, "theme"));
try {
readSetting(broken, "theme");
} catch (err) {
console.log("propagated:", err.name, "-", err.message);
}Example explained
Line 1Calling store.get on a plain object throws a TypeError, the one shape of failure this function knows how to replace with a default.
Line 2throw err passes the unexpected RangeError along unchanged instead of pretending "dark" was the stored setting.
Line 3Because the rethrow leaves readSetting as an exception, the caller's try stops at that call and only its catch runs.
Line 4Without the instanceof check, the RangeError would also have returned "dark" and the offline shard would never be reported.
Recovering from a rejected promise
Uses await inside try so a rejection becomes a catchable throw, with finally resetting shared state on both paths.
function getScore(shouldFail) {
return shouldFail
? Promise.reject(new Error("network down"))
: Promise.resolve(42);
}
let loading = false;
async function showScore(shouldFail) {
loading = true;
try {
const score = await getScore(shouldFail);
console.log("live score:", score);
} catch (err) {
console.log("cached score, because:", err.message);
} finally {
loading = false;
console.log("loading =", loading);
}
}
showScore(false).then(() => showScore(true));Example explained
Line 1await converts the rejection into a throw at that exact line, which is the only reason an ordinary catch can see it.
Line 2The console.log after the failing await is skipped, so live score never prints on the second call.
Line 3finally clears loading on both the success and the failure path, so a spinner cannot get stuck after an error.
Line 4The .then chain runs the two calls one after the other; called together they would fight over the same loading flag.
Important notes
catch only intercepts throws that happen while its try block is still on the stack; a throw inside a setTimeout callback, or an unawaited promise rejection, escapes it entirely.
Any value can be thrown, not just Error objects, so err.message may be undefined; if you do not need the value at all, ES2019 allows catch with no parameter.
Common mistakes
Wrapping an entire function body in one try: a typo that throws TypeError is caught by the handler written for JSON.parse, so the function quietly returns its fallback and the real bug stays invisible.
Leaving catch empty, or writing catch { return null } with no log and no rethrow: the program continues on missing data and the symptom appears somewhere unrelated much later.
Putting return in finally to keep a function tidy: it overwrites the try block's completion, so a thrown error is discarded and callers are told the operation succeeded.
Try it yourself
Change, predict, then run
In a browser console, write parseList(text) that returns JSON.parse(text) from a try block, returns [] from catch, and logs "attempted" in finally. Call it with '[1,2]' and with '[1,2' and confirm that "attempted" prints before the returned value in both cases.
Open the JavaScript workspaceCheck your understanding
A function's try block ends with return computeValue(); and its finally block runs console.log("closed"). What is the order of events, and what does the caller receive?
- "closed" logs before computeValue() runs, because finally is prepared first
- The caller receives the value, then "closed" logs afterwards
- computeValue() runs, then "closed" logs, then the caller receives that value
- "closed" never logs, because return leaves the function immediately
Show answer
return first evaluates computeValue() and stores the result as a pending completion, then finally runs, and only then does control leave the function, so cleanup is guaranteed but cannot change the value unless finally itself returns. Option 4 is tempting because return normally ends a function on the spot, but finally is honoured on every exit path, including return and throw.