Command Palette

Search for a command to run...

Hectal
PHASE 13Intermediate ~22 min· topic 2 of 6

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 inside catch (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.

shape.jswhole filejavascript
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");
}
The shape of a try statementdiagram
Rendering diagram…

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.

settings.jswhole filejavascript
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.

save.jswhole filejavascript
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.

storage-check.jswhole filejavascript
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.

timers.jswhole filejavascript
// 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. 1

    Remove the rethrow

    In the second example, delete the line throw err;. Predict what loadTheme("{}") returns now and what the caller prints. Why is that worse?

  2. 2

    Return from finally

    In the first example, add return "from finally"; as the last line of the finally block. Predict both Result: lines, then run it.

  3. 3

    Forget the await

    In the third example, delete await before Promise.reject(...). Predict: does the catch still run? Look for an Uncaught (in promise) line.

Code & diagrams

The path through try, catch and finally New tab

Even though `try` and `catch` both `return`, `finally` runs before the value reaches the caller.

Preview

Press Run to see the page.

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: null
Catch what you expect, rethrow the rest New tab

If `loadTheme` returned "LIGHT" for every error, the missing `theme` bug would hide forever.

Preview

Press Run to see the page.

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')
try only sees errors while its block runs New tab

The outer `catch` never runs: by the time the timers fire, that `try` finished long ago.

Preview

Press Run to see the page.

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: SyntaxError
finally resets the page on both paths ES2017+ New tab

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Result: saved
finally: status back to Idle
Failed: network down
finally: status back to Idle

Break 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.

terminal
$ the page shows
── what you'll see ──
Nothing happens and the console is empty: no total, no error.

Break #2

return inside finally

Add return "done"; inside finally in a function whose try throws an error you meant to pass up.

terminal
$ Console
── what you'll see ──
The function returns "done" and the error disappears without a trace.

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/finally with completion records: each block ends with a normal, return, throw, break or continue completion, and a non-normal completion from finally overrides the earlier one. That's exactly why return inside finally swallows a thrown error.

  • ▸

    window.addEventListener("error", ...) receives uncaught errors (and, in the capture phase, failed resource loads like broken images); unhandledrejection receives promise rejections nobody handled. Error-tracking tools such as Sentry hook these to send reports. Calling event.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> has crossorigin and the server sends a CORS header. Same-origin scripts give full details.

  • ▸

    Errors thrown inside a catch or finally replace the original error, which is lost unless you keep it. The ES2022 cause option (Topic 13.3) lets you wrap an error and keep the original attached.

Remember this

  1. 1

    A **try statement** has a try block and at least one of catch or finally. The engine runs the try block line by line. If nothing throws, the catch block is skipped. If any line throws (directly, or deep inside a function it calls), the engine jumps straight to catch: the rest of the try block doesn't run. The thrown value arrives as the catch parameter, usually named err or error: catch (err) { ... }. After catch finishes, the program carries on after the whole statement as if nothing happened.

  2. 2

    **finally runs always**: after try succeeds, after catch handles an error, and even when try or catch leaves early with return, break or continue, 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 write catch { ... } without the parameter; this optional catch binding arrived in ES2019.

  3. 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/catch makes bugs harder to find, not easier.

  4. 4

    Rethrowing: a catch block catches every error from the try block, 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 with throw err;: if (!(err instanceof SyntaxError)) throw err;. Then unexpected bugs still reach the console or a caller instead of being quietly swallowed.

  5. 5

    try/catch only sees errors thrown while its block is running. A callback passed to setTimeout or addEventListener runs later, after the try has finished, so an error inside it is not caught by the outer try: put the try inside the callback. Promises work the same way: a rejected promise is caught only if you await it inside the try (in an async function, Topic 10.8) or attach .catch() (Topic 10.6).

  6. 6

    Variables declared with let or const inside try belong to that block and can't be seen after it. Declare the variable before the try if you need it later, or return from inside the try. As a last resort, a page can listen for the window error event (uncaught errors) and the unhandledrejection event (unhandled promise rejections) to report problems to a server; that's for logging, not for recovering.

Explain it without notes

01

Walk through what happens, step by step, when a line in a try block throws.

02

When should you catch an error, and when should you let it pass?

03

Why doesn't a try around setTimeout catch an error thrown in the callback, and how do you fix it?

04

What is finally for, and what goes wrong if you return from it?

Practice

01

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.)

02

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.warn or an error report so someone finds out.

Done when you can

  • I can trace which lines run when a try block throws.

  • I use finally for cleanup that must always happen.

  • I handle only the errors I expect and rethrow the rest.

  • I never leave a catch block empty.

  • I know try doesn't catch errors in later callbacks or un-awaited promises.