Topic 13.2
try, catch and finally
In one line
try runs code that might fail, catch runs only if it throws and receives the error, and finally runs in every case; catch an error only where you can do something useful with it, and rethrow the ones you can't handle.
Think of it like this
A trapeze artist and a safety net. The artist tries the jump (try). Most of the time it works and the net is never used. If they fall, the net catches them (catch) and the show goes on instead of ending in disaster. Whatever happens, at the end the crew rolls the equipment away (finally). A net under the wrong act is useless, though: you put it where a fall can actually happen.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- try block
- The code that might throw an error.
- catch block
- The code that runs only if the try block threw, and receives the error.
- finally block
- Code that runs after try and catch every time, success or failure.
- Catch parameter
- The name, like
err, that holds the thrown value insidecatch (err) { ... }. - Rethrow
- Throw the same error again from a catch block, because this code can't handle it.
- Swallow an error
- Catch it and do nothing, so nobody ever finds out it happened.
- Fallback
- A safe value or behaviour used when the normal way fails.
- Optional catch binding
- Writing
catch { }without(err)when you don't need the error (ES2019).
Step by step
01The shape of a try statement
The engine runs try. On success it skips catch; on failure it jumps to catch. Then finally runs either way, and the program continues after the statement.
try {
// code that might throw
riskyStep();
console.log("only runs if riskyStep did not throw");
} catch (err) {
// runs only when something in try threw
console.log("Handled:", err.message);
} finally {
// runs every time
console.log("cleanup");
}02Catch only what you can fix
Here JSON.parse might throw a SyntaxError for bad text. That's expected, and we have a sensible fallback. Any other error means a real bug, so it goes back up with throw err.
function readSettings(text) {
try {
return JSON.parse(text);
} catch (err) {
if (err instanceof SyntaxError) {
return { theme: "light" }; // expected: bad saved data
}
throw err; // unexpected: let a caller or the console see it
}
}03Clean up in finally
Code that sets something up (a spinner, a disabled button) must undo it on success and on failure. Putting the undo in finally means you write it once and can't forget a path.
async function save(data) {
button.disabled = true;
try {
await sendToServer(data);
showMessage("Saved");
} catch (err) {
showMessage("Could not save. Try again.");
} finally {
button.disabled = false; // runs on both paths
}
}04Leave out the parameter when you don't need it
Since ES2019, catch can skip (err). Use it when the kind of failure doesn't matter, such as feature checks.
function storageWorks() {
try {
localStorage.setItem("test", "1");
localStorage.removeItem("test");
return true;
} catch {
return false; // blocked, full, or private mode: all mean "no"
}
}05Put the try where the code runs
A try around setTimeout(...) only protects the act of scheduling. The callback runs later, on its own, so its errors escape. Move the try into the callback, or use async/await so the error comes back into your function.
// Doesn't work: the callback runs after this try has finished
try {
setTimeout(() => JSON.parse("oops"), 100);
} catch (err) { /* never runs */ }
// Works: the try is inside the code that runs later
setTimeout(() => {
try { JSON.parse("oops"); } catch (err) { console.log(err.name); }
}, 100);Try it yourself
- 1
Remove the rethrow
In the second example, delete the line
throw err;. Predict whatloadTheme("{}")returns now and what the caller prints. Why is that worse? - 2
Return from finally
In the first example, add
return "from finally";as the last line of thefinallyblock. Predict bothResult:lines, then run it. - 3
Forget the await
In the third example, delete
awaitbeforePromise.reject(...). Predict: does thecatchstill run? Look for anUncaught (in promise)line.
Code & diagrams
Even though `try` and `catch` both `return`, `finally` runs before the value reaches the caller.
Preview
Console
Nothing logged yet.Expected console output
try: reading {"age": 12}
try: parsed OK
finally: always runs
Result: 12
try: reading {age: 12}
catch: SyntaxError
finally: always runs
Result: nullIf `loadTheme` returned "LIGHT" for every error, the missing `theme` bug would hide forever.
Preview
Console
Nothing logged yet.Expected console output
DARK
Bad JSON, using the default theme
LIGHT
The caller caught: TypeError: Cannot read properties of undefined (reading 'toUpperCase')The outer `catch` never runs: by the time the timers fire, that `try` finished long ago.
Preview
Console
Nothing logged yet.Expected console output
try block finished
await inside try caught: server said no
Uncaught Error: thrown from a timer
try inside the callback caught: SyntaxErrorPreview
Console
Nothing logged yet.Expected console output
Result: saved
finally: status back to Idle
Failed: network down
finally: status back to IdleBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Swallow every error
Write try { const total = prise * 2; show(total); } catch (err) {} with a typo in price.
Break #2
return inside finally
Add return "done"; inside finally in a function whose try throws an error you meant to pass up.
Myth vs fact
Myth
Wrapping everything in try/catch makes code safer.
Fact
It makes failures quieter, not fewer. Catch where you can recover or report, and let other errors travel up to code that can.
Myth
try/catch catches errors in callbacks and promises started inside it.
Fact
It only sees errors thrown while its block runs. Later callbacks need their own try, and promises need await inside the try or a .catch().
Myth
try/catch makes JavaScript slow.
Fact
That was true in old engines. Since 2017, V8's TurboFan optimises functions that contain try/catch. Throwing is still slower than a normal return, so don't use errors for everyday control flow.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The spec models
try/finallywith completion records: each block ends with a normal, return, throw, break or continue completion, and a non-normal completion fromfinallyoverrides the earlier one. That's exactly whyreturninsidefinallyswallows a thrown error. - ▸
window.addEventListener("error", ...)receives uncaught errors (and, in the capture phase, failed resource loads like broken images);unhandledrejectionreceives promise rejections nobody handled. Error-tracking tools such as Sentry hook these to send reports. Callingevent.preventDefault()on them hides the console message, so do that only after you've recorded the error. - ▸
Scripts from another origin report uncaught errors as just
Script error.with no details unless the<script>hascrossoriginand the server sends a CORS header. Same-origin scripts give full details. - ▸
Errors thrown inside a
catchorfinallyreplace the original error, which is lost unless you keep it. The ES2022causeoption (Topic 13.3) lets you wrap an error and keep the original attached.
Remember this
- 1
A **
trystatement** has atryblock and at least one ofcatchorfinally. The engine runs thetryblock line by line. If nothing throws, thecatchblock is skipped. If any line throws (directly, or deep inside a function it calls), the engine jumps straight tocatch: the rest of thetryblock doesn't run. The thrown value arrives as the catch parameter, usually namederrorerror:catch (err) { ... }. Aftercatchfinishes, the program carries on after the whole statement as if nothing happened. - 2
**
finallyruns always**: aftertrysucceeds, aftercatchhandles an error, and even whentryorcatchleaves early withreturn,breakorcontinue, or throws again. Use it for cleanup that must happen whatever the outcome: turning off a loading spinner, re-enabling a button, closing a file. If you don't need the error object, you can writecatch { ... }without the parameter; this optional catch binding arrived in ES2019. - 3
When to catch: only where you can do something useful: show the user a clear message, use a default value, try again, or record the error and continue with other work. A function that parses saved settings can catch bad JSON and fall back to defaults. A low-level helper that can't fix anything should let errors pass up to a caller that can. Wrapping every line in
try/catchmakes bugs harder to find, not easier. - 4
Rethrowing: a
catchblock catches every error from thetryblock, including ones you didn't plan for, like a typo that causes a ReferenceError. So check what you got, handle only that kind, and rethrow the rest withthrow err;:if (!(err instanceof SyntaxError)) throw err;. Then unexpected bugs still reach the console or a caller instead of being quietly swallowed. - 5
try/catchonly sees errors thrown while its block is running. A callback passed tosetTimeoutoraddEventListenerruns later, after thetryhas finished, so an error inside it is not caught by the outertry: put thetryinside the callback. Promises work the same way: a rejected promise is caught only if youawaitit inside thetry(in anasyncfunction, Topic 10.8) or attach.catch()(Topic 10.6). - 6
Variables declared with
letorconstinsidetrybelong to that block and can't be seen after it. Declare the variable before thetryif you need it later, or return from inside thetry. As a last resort, a page can listen for the windowerrorevent (uncaught errors) and theunhandledrejectionevent (unhandled promise rejections) to report problems to a server; that's for logging, not for recovering.
Explain it without notes
Walk through what happens, step by step, when a line in a try block throws.
When should you catch an error, and when should you let it pass?
Why doesn't a try around setTimeout catch an error thrown in the callback, and how do you fix it?
What is finally for, and what goes wrong if you return from it?
Practice
Write toNumber(text) that returns Number(text) when the result is a real number, and otherwise logs Not a number: <text> and returns 0. (Number doesn't throw, so decide whether you need try at all.)
Write parseOr(text, fallback) that returns JSON.parse(text) or fallback when the text isn't valid JSON, and rethrows any other kind of error. Test it with '[1,2]' and 'oops'.
Trade-offs
- ↔
Catching close to the failure lets you give a precise message or fallback; catching high up (one handler around a whole action) is simpler and catches more, but knows less about what went wrong.
- ↔
Returning a fallback keeps the page working, but can hide that data is broken; pair fallbacks with a
console.warnor an error report so someone finds out.
Done when you can
I can trace which lines run when a
tryblock throws.I use
finallyfor cleanup that must always happen.I handle only the errors I expect and rethrow the rest.
I never leave a
catchblock empty.I know
trydoesn't catch errors in later callbacks or un-awaited promises.