Menu

JavaScript try/catch: Handling Errors Without Crashing

How try/catch/finally works in JavaScript — catching errors, the error object, rethrowing, and when try/catch is the wrong tool.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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. try blocks have real overhead and muddy the code compared to an if check. if (user) beats try { 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 throw Error subclasses so stack traces work.
  • throw err; inside catch — rethrow when you can't handle it.
  • await inside try — required for try/catch to 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.

Learn to code with Coddy

GET STARTED