Error Handling

Dealing with things that go wrong in your code on purpose, instead of letting the program crash.

What is it?

Sometimes code can't do what it was asked — a network request fails, a file doesn't exist, a value isn't what was expected. Left alone, this throws an error, and unless something handles it, the program crashes. JavaScript's try/catch lets you say: "attempt this, and if it fails, run this other code instead of crashing."

Explain like I'm 10

It's like a safety net under a tightrope walker. You still attempt the risky move (the try), but if something goes wrong, the net (the catch) stops it from becoming a disaster.

Examples

try / catch / finally

function parseUserAge(input) {
  try {
    const age = JSON.parse(input);
    if (typeof age !== "number") {
      throw new Error("Age must be a number");
    }
    return age;
  } catch (error) {
    console.log("Invalid input:", error.message);
    return null;
  } finally {
    console.log("Finished attempting to parse.");
  }
}

A custom error class

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

function setAge(age) {
  if (age < 0) {
    throw new ValidationError("Age can't be negative");
  }
  return age;
}

try {
  setAge(-5);
} catch (error) {
  if (error instanceof ValidationError) {
    console.log("Validation failed:", error.message);
  } else {
    throw error; // an error we didn't expect — let it propagate
  }
}

Custom error classes let a catch block tell different kinds of failures apart with instanceof, instead of treating every error the same way.

How it works

When code inside try throws (either automatically, from a failing operation, or manually via throw), JavaScript immediately stops executing that block and jumps to the matching catch, skipping any remaining lines in try. The optional finally block runs afterward no matter what happened, useful for cleanup that must always happen.

Why does it exist?

Without error handling, one unexpected failure anywhere would crash the entire program. try/catch lets you contain failures to the specific operation that caused them, respond sensibly (retry, show a message, use a default), and keep the rest of the program running.

When to use it

Wrap code in try/catch around operations that can realistically fail in ways you want to handle gracefully — parsing untrusted data, network requests (often alongside async/await), or any operation whose failure shouldn't crash the whole app.

When not to use it

Don't wrap code in try/catch just out of caution when there's no realistic failure to handle, or nothing sensible to do in the catch block — silently swallowing errors that way can hide real bugs instead of fixing them. And don't use exceptions for ordinary control flow (like checking if a key exists) when a simple if would do.

Common mistakes

  • Catching an error and doing nothing with it (an empty catch block), which hides bugs instead of fixing them.

  • Forgetting that catch only catches errors thrown synchronously inside the try — a callback or an unawaited promise inside it can still throw unnoticed.

  • Throwing plain strings or objects instead of an Error (or subclass), losing useful information like a stack trace.

Practice exercises

  1. Easy:

    Write a function that safely divides two numbers, throwing an error if the divisor is 0, and catching it to return null instead of crashing.

  2. Medium:

    Create a custom error class ValidationError extends Error and throw/catch an instance of it.

  3. Hard:

    Write an async function that retries a failing operation up to 3 times before giving up, using try/catch inside a loop.

Interview questions

Does `finally` still run if the `try` block contains a `return` statement?

Yes — finally always runs before the function actually returns, no matter whether try completed normally, threw an error that got caught, or hit a return directly; there's no way to skip it short of the whole process crashing.

Predict the output: `function f() { try { return 1; } finally { return 2; } } console.log(f());`

2 — a return inside finally completely overrides whatever try (or catch) was about to return, or even an error that was about to propagate. This is a well-known footgun, which is why returning from inside finally is generally avoided.

If an error is already propagating out of a `catch` block, and the paired `finally` block also throws, which error wins?

The one from finally — just like a return inside finally overrides an earlier one, a throw inside finally replaces whatever error was already on its way out, and the original error is lost entirely unless it was explicitly captured beforehand.

Can you write `try { ... } finally { ... }` with no `catch` at all? What happens to an error thrown inside `try` in that case?

Yes, that's valid syntax. finally still runs for cleanup, but since there's no catch to actually handle the error, it continues propagating upward afterward, exactly as if the try/finally wrapper weren't there for error-handling purposes — it only guarantees the cleanup code runs, not that the error is contained.

What is the "optional catch binding" syntax (`catch { ... }` with no parameter), and when is it useful?

It lets you write a catch block without capturing the error into a named variable, for cases where you genuinely don't need any information about what went wrong — just that something did — avoiding an unused variable purely to satisfy the older, mandatory syntax.

Why is throwing a plain string or plain object considered worse practice than throwing an `Error` instance?

An Error object automatically captures a stack trace and a consistent message/name shape the moment it's created, which is invaluable for debugging where and why something failed. A thrown string or plain object carries none of that — you lose the call-site information entirely.

What is `error.stack`, and where does its content actually come from?

It's a string, automatically populated when an Error (or subclass) is constructed, showing the sequence of function calls that were active at that moment — the call stack — which is why creating the Error object itself (not just throwing it) is what captures the trace.

What does the `cause` option in `new Error(message, { cause: originalError })` let you do?

It lets you attach an earlier, underlying error onto a new, higher-level one you're throwing instead — for example wrapping a low-level network error inside a more descriptive "Failed to load user" error — while preserving the original error (accessible via .cause) instead of discarding it.

Inside an `async` function's `try` block, does `catch` reliably handle a promise that's created but never `await`ed?

No — try/catch only catches a rejection at the exact point where await is used on that promise. A promise that's fired off without being awaited (or returned) inside the try can still reject later, completely outside the try/catch's control, becoming an unhandled rejection instead of being caught there.

Why doesn't a `try/catch` wrapped around `setTimeout(() => { throw new Error("boom"); }, 0)` catch that thrown error?

The try/catch block has already finished executing (its synchronous code ran and returned) long before the timer callback actually runs later, on its own turn through the event loop — by the time the error is thrown, there's no try/catch still "active" to catch it; the error has to be caught inside the callback itself.

How do you correctly handle an error from an older, callback-style asynchronous function (not one that returns a promise)?

Check for the error argument inside the callback itself (the common "error-first callback" convention), since a try/catch wrapped around the outer call can't reach into an error thrown later, inside a callback that runs on a different turn of the event loop.

What's the danger of an empty `catch` block that does nothing with the error it caught?

It silently swallows real failures — the program keeps running as if nothing went wrong, hiding bugs that would otherwise have been visible (via a crash or a log), often making the eventual symptom much harder to trace back to its actual cause.

How do you handle several different kinds of errors differently inside a single `catch` block?

Check the caught error's type with instanceof, from most specific to least specific (since a subclass is also an instance of its parent class), branching your handling logic accordingly — JavaScript doesn't support multiple typed catch clauses the way some other languages do, so this manual branching is the idiomatic replacement.

What are some of JavaScript's built-in `Error` subtypes, and what typically triggers each one?

TypeError for using a value in a way its type doesn't support (like calling a property that isn't a function, or reading a property off null/undefined); RangeError for a value outside an allowed range (like an invalid array length); ReferenceError for referencing a name that doesn't exist; and SyntaxError for malformed code or malformed input to something like JSON.parse.

Predict what each of these throws: `null.foo`, referencing an undeclared variable, and `JSON.parse("{bad")`.

null.foo throws a TypeError (you can't read a property off null). Referencing an undeclared variable throws a ReferenceError. JSON.parse("{bad") throws a SyntaxError, since the text isn't valid JSON. Knowing which is which lets a catch block respond appropriately instead of treating every failure identically.

When should you rethrow an error you just caught, instead of handling it right there?

When the current code doesn't actually have enough context or ability to do anything meaningful about the failure — logging it and swallowing it would just hide the problem from whoever (or whatever layer) actually could handle it correctly, so letting it keep propagating upward is the more honest response.

When is throwing an exception the right call for invalid input, versus just returning early with an `if` check or an error code?

Exceptions fit exceptional, unexpected situations that the immediate caller likely didn't anticipate and can't just check for beforehand (a network failure, a corrupt file). For ordinary, expected conditions a caller should routinely check (an empty search result, a missing optional field), a plain return value or early if/return is usually clearer and cheaper than throwing.

How does extending `Error` correctly (`class ValidationError extends Error { constructor(msg) { super(msg); ... } }`) preserve `instanceof` checks?

Calling super(message) runs Error's own constructor logic, which properly wires up the new object's prototype chain back through Error.prototype, so error instanceof ValidationError and error instanceof Error both correctly return true — skipping super() (or not extending Error at all) breaks that chain.

Is it possible for `error instanceof MyCustomError` to fail even when the error really was thrown as `new MyCustomError(...)`?

Yes, in some unusual setups — for example transpiling class syntax down to an older JS target without special handling for built-in extension can break the prototype chain, since natively extending Error relies on engine behavior that a naive transpilation doesn't always replicate correctly.

What's an "unhandled promise rejection", and how is it different from an uncaught synchronous exception?

It's a promise that rejected with no .catch() (or awaiting try/catch) ever attached to observe the failure. Unlike a synchronous uncaught exception, which typically halts execution immediately at that point, an unhandled rejection is detected asynchronously — the environment (browser or Node) reports it separately, sometimes well after the rejection actually happened.

How would you set up a last-resort handler to catch errors that slipped past every other `try/catch` in an app?

In a browser, listen for the global error event (or set window.onerror) for uncaught synchronous exceptions, and the unhandledrejection event for unhandled promise rejections. In Node.js, process.on("uncaughtException", ...) and process.on("unhandledRejection", ...) serve the same purpose — though these are meant as a last-resort safety net for logging/cleanup, not a substitute for handling errors closer to where they happen.

If every code path inside a `try` block ends in a `return`, does the paired `finally` block still get a chance to run before the function actually returns?

Yes — finally always runs immediately before control actually leaves the function, regardless of which return path was taken inside try, since finally's entire purpose is to guarantee cleanup runs no matter how the try/catch is exited.

What's the tradeoff between wrapping a `try/catch` around each iteration of a loop individually versus wrapping the entire loop in one `try/catch`?

Catching per-iteration lets the loop continue processing the remaining items even if one fails, at the cost of slightly more code; wrapping the whole loop stops everything the moment any single iteration fails, which is simpler but means one bad item can prevent all the others from ever being processed.

Why is it risky to rely on one giant `try/catch` around an entire application as the main error-handling strategy?

It tends to catch and quietly swallow a huge variety of unrelated failures in one place, far from where each actually happened and with little context to respond appropriately — real bugs get hidden behind a generic "something went wrong" instead of being handled (or at least surfaced clearly) close to their source.

Can a single `try` block have multiple `catch` clauses for different error types, the way some other languages allow?

No — JavaScript only supports one catch block per try. Distinguishing between error types has to happen manually inside that one catch, typically with instanceof checks.

What happens if the code inside a `catch` block itself throws a new error?

That new error propagates upward from the catch block exactly like any other thrown error would — it isn't caught by the very same catch block that produced it; you'd need an enclosing try/catch around the whole thing to handle a failure that happens during error handling itself.

Is there a meaningful runtime performance penalty to wrapping code in `try/catch` in modern JavaScript engines?

Not really, for the common case — modern engines optimize try/catch blocks well, and the old advice to avoid them for performance reasons is largely outdated; the far more important consideration is using them where they add clarity and safety, not micro-optimizing them away.

Why should error messages meant for logging or debugging often be different from what's shown directly to an end user?

A log-facing message can safely include technical detail (stack traces, internal identifiers, exact failure reasons) that's useful for diagnosing the problem but meaningless or even risky (leaking internals) to show a user, who generally needs a simpler, actionable message instead.

What's the benefit of a function throwing early ("failing fast") on invalid input, instead of letting bad data quietly propagate deeper into the program?

It surfaces the problem at its actual source, with a clear, specific error message and a stack trace pointing at the real cause — instead of the program continuing on with corrupted data and failing confusingly much later, somewhere that has no direct connection to where things actually went wrong.

How would you write a function that retries a flaky operation up to a fixed number of times before finally giving up?

Loop up to the retry limit, wrapping the attempt in try/catch each time: on success, return immediately; on failure, catch the error and either continue to the next attempt, or, once the limit is reached, rethrow the last error (or return an explicit failure) instead of silently giving up.