JavaScript Interview Prep
Error Handling

try...catch...finally

The Safety Net That Never Lets Go

LinkedIn Hook

You wrote try { risky() } catch (e) { console.log(e) } and shipped it. Done, right?

Not quite. There's a corner of try...catch...finally that trips up even senior engineers — the moment finally meets return. One return inside finally can silently override everything your try and catch just did. No warning. No error. Just a production bug that takes three hours to reproduce.

In this lesson you will learn the three blocks, why finally ALWAYS runs (even with return, even with throw), how optional catch binding (ES2019) cleaned up a 20-year annoyance, the performance cost of throwing errors in tight loops, and the right way to re-throw errors you cannot handle.

If you have ever wondered "why did my function return the wrong value even though catch clearly returned the right one?" — read this lesson.

Read the full lesson -> [link]

#JavaScript #InterviewPrep #ErrorHandling #TryCatch #Frontend #NodeJS #CodingInterview #WebDevelopment


try...catch...finally thumbnail


What You'll Learn

  • The three blocks of try...catch...finally, which are optional, and the exact order they execute in
  • Why finally always runs — and how a return inside finally can override try/catch
  • Optional catch binding (ES2019), nested try/catch, re-throwing, and the performance cost of throwing errors

The Trapeze Act

Think of try...catch...finally like a trapeze act at a circus. The performer (your code) swings on the trapeze (try). If they fall, the safety net (catch) catches them. And no matter what happens — perfect landing or dramatic fall — the lights still come on at the end (finally).

Basic Syntax

try {
  // Code that might throw an error
  const result = riskyOperation();
  console.log(result);
} catch (error) {
  // Runs ONLY if try block throws
  console.log("Something went wrong:", error.message);
} finally {
  // Runs ALWAYS — whether try succeeded or catch ran
  console.log("Cleanup complete");
}

The error object in catch has three useful properties:

  • error.message — human-readable description
  • error.name — the error type (e.g., "TypeError")
  • error.stack — the full stack trace (invaluable for debugging)

Optional Catch Binding (ES2019)

Since ES2019, you can omit the error parameter in catch if you don't need it:

// Before ES2019 — had to declare the parameter even if unused
try {
  JSON.parse("invalid json");
} catch (e) {
  // 'e' declared but never used
  console.log("Parse failed");
}

// ES2019+ — catch without parameter
try {
  JSON.parse("invalid json");
} catch {
  // No parameter needed
  console.log("Parse failed");
}

This is purely syntactic sugar — the behavior is identical. It's useful when you only care that an error occurred, not what the error was.

The Surprising finally + return Behavior

This is one of the most surprising behaviors in JavaScript, and a favorite interview question. finally runs even if try or catch has a return statement — and finally can OVERRIDE the return value.

function surprisingReturn() {
  try {
    return "from try";
  } finally {
    return "from finally"; // This OVERRIDES the try's return!
  }
}

console.log(surprisingReturn()); // "from finally" — NOT "from try"!
// finally runs even with return in catch
function anotherSurprise() {
  try {
    throw new Error("oops");
  } catch (e) {
    return "from catch";
  } finally {
    return "from finally"; // Still overrides!
  }
}

console.log(anotherSurprise()); // "from finally"
// finally runs even with thrown errors
function finallyAlwaysRuns() {
  try {
    throw new Error("try error");
  } catch (e) {
    throw new Error("catch error");
  } finally {
    console.log("I still run!"); // This WILL execute
    // If finally doesn't have a return/throw, the catch's throw propagates
  }
}

Rule: finally ALWAYS runs. If finally has a return, it overrides any return or throw from try or catch. This is why ESLint has a no-unsafe-finally rule — returning from finally is almost always a bug.

Nested try/catch

You can nest try/catch blocks. Inner errors can be caught by inner catch, and if re-thrown, bubble up to the outer catch.

try {
  console.log("Outer try");

  try {
    console.log("Inner try");
    throw new Error("inner error");
  } catch (innerError) {
    console.log("Inner catch:", innerError.message);
    throw new Error("re-thrown from inner catch");
  } finally {
    console.log("Inner finally");
  }

} catch (outerError) {
  console.log("Outer catch:", outerError.message);
} finally {
  console.log("Outer finally");
}

// Output:
// "Outer try"
// "Inner try"
// "Inner catch: inner error"
// "Inner finally"
// "Outer catch: re-thrown from inner catch"
// "Outer finally"

Re-throwing Errors

Sometimes you catch an error, check if it's the kind you can handle, and re-throw if it's not:

function processData(data) {
  try {
    return JSON.parse(data);
  } catch (error) {
    if (error instanceof SyntaxError) {
      // We know how to handle JSON parse errors
      console.log("Invalid JSON, returning default");
      return { default: true };
    }
    // Unknown error — re-throw it
    throw error;
  }
}

try/catch Performance Considerations

// BAD — try/catch wrapping tight loops
function slowApproach(arr) {
  const results = [];
  for (let i = 0; i < arr.length; i++) {
    try {
      results.push(arr[i].toString());
    } catch (e) {
      results.push("error");
    }
  }
  return results;
}

// BETTER — validate before, catch outside
function fastApproach(arr) {
  try {
    const results = [];
    for (let i = 0; i < arr.length; i++) {
      if (arr[i] != null) {
        results.push(arr[i].toString());
      } else {
        results.push("error");
      }
    }
    return results;
  } catch (e) {
    // Handle truly unexpected errors
    return [];
  }
}

Performance tip: Modern engines (V8, SpiderMonkey) have optimized try/catch significantly. The overhead is minimal when no error is thrown. However, actually THROWING and CATCHING errors is expensive — it involves capturing a stack trace. Use validation (if checks) for expected cases, and try/catch for truly exceptional situations.

try...catch...finally visual 1


Common Mistakes

  • Returning from finally — it silently overrides the return or throw from try/catch. ESLint's no-unsafe-finally rule flags this because it is almost always a bug.
  • Wrapping a tight loop with try/catch when you could validate first — the cost is not entering the try, it is actually throwing (stack-trace capture is expensive).
  • Swallowing errors with catch (e) { } or catch (e) { console.log(e) } — unknown errors should be re-thrown so the caller (or a global handler) can deal with them.

Interview Questions

Q: What are the three blocks in error handling and which are optional?

try, catch, and finally. try is required. You need at least ONE of catch or finallytry alone is a syntax error. try { } finally { } (no catch) is valid if you want cleanup but want the error to propagate.

Q: Can you use try without catch?

Yes, but only if you include finally. try...finally is valid — useful when you need cleanup code to run regardless of success/failure, but want the error to propagate naturally.

Q: What happens if both try and finally have a return statement?

The finally block's return value wins. finally always executes, and if it contains a return, it overrides any return value from try or catch. This is generally considered a bug and should be avoided.

Q: What is optional catch binding?

Since ES2019, you can write catch { } without a parameter instead of catch (error) { }. This is useful when you don't need the error object — you just need to know that something failed.

Q: Does try/catch have a performance cost?

Entering a try block has negligible cost in modern engines. However, actually throwing an error is expensive because JavaScript must capture the full stack trace. For expected conditions, prefer validation (if/else) over try/catch.

Q: Why is throwing errors inside tight loops a bad idea?

Each throw forces the engine to capture a full stack trace, which is expensive. Inside a hot loop, that cost multiplies per iteration. Validate inputs with if checks and keep the try/catch around the loop (or outside it) for truly exceptional cases.


Quick Reference — Cheat Sheet

TRY / CATCH / FINALLY — QUICK MAP

Shape:
  try        -> required
  catch (e)  -> optional (parameter optional since ES2019)
  finally    -> optional
  At least one of catch/finally must be present.

Execution order:
  try runs -> if throws -> catch runs -> finally runs (always)
  try runs -> if succeeds            -> finally runs (always)

Golden rules:
  - finally ALWAYS runs (even on return, even on throw)
  - return in finally OVERRIDES return/throw in try/catch (BUG)
  - ESLint: no-unsafe-finally catches this
  - Validate in the hot path; throw only for the exceptional

error object:
  .message  -> human-readable description
  .name     -> "TypeError", "SyntaxError", ...
  .stack    -> full stack trace

Previous: Proxy & Reflect Next: Custom Errors -> Building an Error Hierarchy


This is Lesson 10.1 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page