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...finallythat trips up even senior engineers — the momentfinallymeetsreturn. Onereturninsidefinallycan silently override everything yourtryandcatchjust 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
finallyALWAYS runs (even withreturn, even withthrow), 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
What You'll Learn
- The three blocks of
try...catch...finally, which are optional, and the exact order they execute in - Why
finallyalways runs — and how areturninsidefinallycan overridetry/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 descriptionerror.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.
Common Mistakes
- Returning from
finally— it silently overrides thereturnorthrowfromtry/catch. ESLint'sno-unsafe-finallyrule flags this because it is almost always a bug. - Wrapping a tight loop with
try/catchwhen you could validate first — the cost is not entering thetry, it is actually throwing (stack-trace capture is expensive). - Swallowing errors with
catch (e) { }orcatch (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, andfinally.tryis required. You need at least ONE ofcatchorfinally—tryalone 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...finallyis 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
finallyblock'sreturnvalue wins.finallyalways executes, and if it contains areturn, it overrides any return value fromtryorcatch. 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 ofcatch (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
tryblock 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
ifchecks and keep thetry/catcharound 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.