JavaScript Try Catch Finally: How Throw and Errors Work
JavaScript throw sends an error up the call stack to the nearest catch, running every finally on the way. What finally overrides, Error cause and the stack.
- Course: JavaScript study plan
- Module: Errors and error handling
- Kind: Lesson
- Reading time: 12 min
- Runtime: Node 16
How do try, catch and finally work in JavaScript?
In JavaScript, throw stops the current function and passes a value up the call stack until a catch receives it; if nothing does, Node prints the error and exits with code 1. A catch takes everything thrown inside its try, bugs included. finally runs after try and catch on every path — normal completion, return or a propagating throw — which makes it the place for cleanup.
Lesson
Errors in JavaScript are values that travel: throw hands one up the call stack until some catch takes it, running every finally on the way. The rules are small but the corners are sharp — a finally can override a return, you can throw anything (and people do), catch catches everything including bugs, and in async code the stack it unwinds is not the one you think (module 8). This lesson pins the synchronous mechanics down exactly, describes what an Error object carries, and sets the vocabulary — throw, propagate, catch, rethrow, wrap — the rest of the module uses.
throw
throw new Error("amount must be positive");
throw new RangeError(`age ${age} out of range`); // a built-in subtype (next lesson)
throw "oops"; // legal, and a bad idea
throw takes any value. Throwing a non-Error — a string, an object literal — loses the stack trace, breaks instanceof Error checks and err.message reads downstream, and shows up in logs as Uncaught oops with no location. Always throw an Error (or subclass). A linter rule (no-throw-literal) enforces it.
Execution stops at the throw: nothing after it in the function runs, and the same is true of every caller up the stack until a catch is found. If none is found the exception is uncaught: Node prints it and exits with code 1; a browser logs it and continues with the next event.
try / catch
try {
const data = JSON.parse(text); // may throw SyntaxError
use(data);
} catch (err) {
console.error("bad input:", err.message);
}
catch receives whatever was thrown. It catches everything thrown inside the try block — including TypeErrors from your own bugs — so a catch that recovers silently can hide real defects. Check what you caught (if (err instanceof SyntaxError) … else throw err) or keep the try block small enough that only the expected failure can occur in it. Since 2019 the binding is optional: catch { … } when you do not need the error.
Variables declared with let/const inside try are not visible in catch or after — declare before the try if you need the value later.
finally
const handle = open(path);
try {
return process(handle);
} finally {
handle.close(); // runs whether process returned, threw, or the function returned early
}
finally runs after try/catch finish, no matter how: normal completion, return, break, continue, or a throw that is propagating. It is the place for cleanup — releasing a lock, closing a handle, restoring state — because it is the only construct guaranteed to run on every path. Two corners:
- A
returnorthrowinsidefinallyoverrides whatever thetry/catchwas completing with — areturninfinallyswallows a propagating exception. Do not return fromfinally. finallyruns even whencatchrethrows — the order istry→catch→finally→ the exception continues upward.
try/finally without a catch is common and correct: "I do not handle this, but I must clean up."
The Error object
const err = new Error("disk full", { cause: originalError }); // cause: ES2022, Node 16.9+
err.name; // "Error" — subclasses set their own
err.message; // "disk full"
err.stack; // "Error: disk full\n at save (app.js:12:9)\n at …" — a string, captured at construction
err.cause; // the wrapped error
String(err); // "Error: disk full" (name: message)
stack is captured when the object is created, not when thrown — creating errors ahead of time gives misleading traces. It is an ordinary string; there is no structured API in the language (Node's Error.captureStackTrace and Error.prepareStackTrace are V8 extras). message and stack are own, non-enumerable properties, so JSON.stringify(err) gives {} — serialise explicitly ({ name, message, stack }) for logs and APIs.
Rethrow and wrap
try { return JSON.parse(text); }
catch (err) {
if (err instanceof SyntaxError) throw new Error(`config ${path} is not valid JSON`, { cause: err });
throw err; // not ours to handle — propagate unchanged
}
Rethrow (throw err) passes an error you cannot handle onward, keeping its stack. Wrap adds context at a layer boundary — the caller learns which config, and the original stays reachable as cause. Never throw new Error(err.message) — it discards the original type, stack and cause. Whether to rethrow or wrap is the subject of lesson 3.
Errors are values
An exception is only special while it is propagating. Once caught it is an ordinary object: store it, return it, put it in an array of failures, attach it to a result. const results = items.map((x) => { try { return { ok: true, value: f(x) }; } catch (err) { return { ok: false, err }; } }); processes a batch without one failure aborting the rest — the result object pattern, again in lesson 3.
Common mistakes
- Throwing strings or plain objects.
catch (e) {}— swallowing everything, including bugs.- Returning from
finally(silently discards exceptions). - Wrapping an error by copying
messageinto a newError— lose type, stack and cause. - A huge
tryblock around a whole function so thecatchcannot know what failed. - Expecting
JSON.stringify(err)to contain the message.
Interview angle
- "What does
finallyguarantee?" It runs on every exit path — normal,return,throw— aftertry/catch; areturninside it overrides the outcome. - "Can you throw a non-Error?" Yes; you should not — no stack, no
message, breaksinstanceof. - "What is
Error.cause?" An option (new Error(msg, { cause })) that keeps the original error when wrapping with context. - "When is the stack captured?" At construction, not at
throw. - "Why is
catch (e) {}dangerous?" It hides bugs (TypeErrors) alongside the expected failure.
Key takeaways
throwany value, but always anError; propagation unwinds callers until acatch.catchtakes everything — check the type or keeptrysmall; the binding is optional.finallyalways runs; neverreturnfrom it;try/finallyalone is fine for cleanup.Errorhasname,message,stack(captured at construction), optionalcause; serialise explicitly.- Rethrow to propagate unchanged; wrap with
{ cause }to add context; caught errors are ordinary values.
Common questions
Does finally run after return in JavaScript?
Yes. finally runs on every exit from the try or catch: normal completion, return, break, continue or a propagating exception. A return or throw inside finally overrides the outcome, so a return there silently swallows an exception — never return from finally.
Can you throw a string in JavaScript?
You can — throw accepts any value — but you should not. A thrown string has no stack trace and no message, fails instanceof Error checks, and shows up in logs as Uncaught oops with no location. Always throw an Error or a subclass; the no-throw-literal lint rule enforces it.
What is the cause option of an Error in JavaScript?
cause is an option of the Error constructor, new Error(message, { cause: err }), added in ES2022 and Node 16.9. It keeps the original error attached when you wrap it with context, so the caller reads the new message and can still reach the original type and stack through err.cause.
Why does JSON.stringify of an Error return {}?
An error's message and stack are own properties but non-enumerable, and JSON.stringify serialises only enumerable ones. Serialise errors explicitly — { name, message, stack } for your logs, and without the stack for anything a user will see.
When is the stack trace of a JavaScript Error captured?
When the Error object is created, not when it is thrown. An error constructed in a helper and thrown somewhere else points at the helper, which is why errors should be created at the point of failure rather than prepared ahead of time.
Exercises
In what order does it run?
Write run(mode, log) with a try/catch/finally. The try pushes "try" and, for modes throw and rethrow, throws new Error("boom"); otherwise it returns "from try". The catch pushes catch <message>, rethrows in mode rethrow, otherwise returns "from catch". The finally pushes "finally" and, in mode override only, returns "from finally". For each input line (a mode) call run inside an outer try/catch and print mode=<mode>: <log joined by ,> -> <returned value> or -> outer caught <message>.
Example: ok, throw, override, rethrow →
mode=ok: try,finally -> from try
mode=throw: try,catch boom,finally -> from catch
mode=override: try,finally -> from finally
mode=rethrow: try,catch boom,finally -> outer caught boom
finally ran every time, and its return replaced the try's.
Safe division with the right error types
Write divide(a, b) that throws TypeError("operands must be finite numbers, got <a> and <b>") unless both are finite numbers and RangeError("division by zero") when b is 0. For each input line a b (converted with Number) print <a> / <b> = <result> or <name>: <message>. Count failures in the catch and every line in the finally; end with processed=<n> failed=<m>.
Example: 10 2, 1 0, x 3, 7 -2 →
10 / 2 = 5
RangeError: division by zero
TypeError: operands must be finite numbers, got NaN and 3
7 / -2 = -3.5
processed=4 failed=2In this module: Errors and error handling
- throw, try, catch, finally — the mechanics, precisely (this lesson)
- The built-in error types, and designing your own
- Where to catch — strategies, boundaries and the result-object alternative
- Validation, guard clauses and assertions — failing early with a useful message
- Reading stack traces and decoding the errors you will see most
- Checkpoint — Errors
← Checkpoint — Collections and iteration · The built-in error types, and designing your own →