try, catch & finally
Catch runtime errors, inspect real Error objects, rethrow failures you cannot handle, and use finally without accidentally hiding a return or error.
- 01Trace protected codePredict exactly when try, catch, and finally run.
- 02Read Error objectsUse name, message, stack, instanceof, and cause safely.
- 03Handle only what you understandUse optional catch binding, rethrowing, and careful finally blocks.
The safety-net idea
JavaScript normally runs one statement after another. If a statement throws and nothing catches it, the current work stops and the error moves up the call stack. You already practiced reading those messages in Reading error messages, and you saw unwinding in The call stack. This lesson adds the tool that lets your program recover on purpose: try, catch, and finally.
Put risky runtime work in try. If it throws, handle the thrown value in catch. Put cleanup that must happen either way in finally.
Imagine changing a light bulb with a safety net under the ladder. You carry on if you slip, then put the ladder away whether you slipped or finished safely. That is the shape of error handling.
- In real life: Changing a light bulb
- In JavaScript: The code inside
try - In real life: Slipping from the ladder
- In JavaScript: A thrown runtime error
- In real life: The safety net
- In JavaScript: The
catchblock - In real life: Putting the ladder away
- In JavaScript: The
finallyblock
Where the analogy stops: A real net catches a fall. A JavaScript catch only handles errors thrown while its matching try block is running. A later timer needs its own catch.
Use try/catch when failure is expected and recoverable: parsing user-provided JSON, reading optional browser storage, or handling a known validation problem. Do not wrap every line “just in case.” A catch block that hides unknown bugs makes debugging harder.
try, catch & finally
STEP THROUGHHere is the whole flow in one tiny program. The path array records exactly which blocks run. Change what happens inside try, then step through the recorded run.
Choose what happens inside try, predict the path, then step through the real run.
script
function riskyFormat(text) { return text.toUpperCase();} let value = "not set"; try { path.push("try"); value = riskyFormat("ok");} catch (error) { path.push("catch " + error.name); value = "fallback";} finally { path.push("finally");} console.log(path.join(" → "));console.log(value);- Success:
tryruns,catchis skipped,finallyruns. - Handled error:
trybegins, a throw jumps tocatch, thenfinallyruns. - Cleanup:
finallyis not “after catch.” It is “before leaving this try statement,” whether the path was success, catch, return, or throw.
This is runtime behavior. A SyntaxError can be thrown by JSON.parse at runtime, so a try block can catch it. But a syntax mistake in the same JavaScript file, such as an unclosed brace, prevents the file from being parsed; the try statement is never created.
The built-in error types
INTERACTIVEMost thrown failures you meet are Error objects. Their most useful fields are name, message, and stack. The name is the category, such as TypeError. The message is a human-readable description. The stack is a string showing where the error was created and how calls got there; its exact format varies by browser.
| Error type | Typical cause | Small example |
|---|---|---|
| SyntaxError | Code cannot be parsed, or JSON text is invalid | JSON.parse("{broken}") inside a try block |
| ReferenceError | A name is not defined in the current scope | missingValue |
| TypeError | A value is the wrong kind for the operation | null.trim() |
| RangeError | A number is outside the allowed range | new Array(-1) |
| URIError | URI encode/decode received malformed escape text | decodeURIComponent("%") |
| EvalError | Historical error type; rarely thrown by modern JavaScript | Mostly seen in compatibility tables |
| AggregateError | Several errors are reported together | Promise.any(...) can create one in asynchronous code |
Click the playground instead of trusting a fixed screenshot. It throws real errors in your browser and catches them immediately so you can inspect what this engine produced.
try { JSON.parse("{broken json}");} catch (error) { console.log(error.name); console.log(error.message); console.log(error.stack?.split("\n")[0]); console.log(error instanceof Error); console.log(error.cause);}Run the example to fill in name, message, stack, instanceof, and cause.
Choose a real error, then run it. The code catches the thrown object and inspects its properties in your browser.
When you need to know whether a caught value is an error, start with error instanceof Error. The earlier Checking types reliably lesson covered instanceof and Error.isError. In ordinary app code, remember that JavaScript can throw any value, so check before reading message.
Optional catch binding
Sometimes the fact that something failed is enough. If you do not need the thrown value, JavaScript lets you omit the binding name: catch { ... }. This is called optional catch binding.
try { JSON.parse(settingsText);} catch { settings = defaultSettings;}That shape is honest when every parse failure gets the same default. If you need to log, inspect error.name, show a specific message, or rethrow unknown failures, use catch (error).
Optional binding is not “cleaner” by default. It is clearer only when the thrown value would be unused noise.
Rethrowing errors you cannot handle
CODE PATTERNA catch block should handle a specific failure it understands. A small net under the ladder catches first; a bigger net catches what it lets through. In code, that handoff is rethrowing: throw error.
The small net handles the slip it was made for. If another problem reaches it, swallowing it would hide the problem. Let it continue to the bigger net instead.
- In real life: The small net catches a slip
- In JavaScript: A catch block handles
ValidationError - In real life: A different problem slips through
- In JavaScript: A bug such as
TypeError - In real life: The bigger net catches it
- In JavaScript:
throw errorreaches a caller or reporter
Where the analogy stops: Rethrowing does not magically fix the error. It preserves the failure so a better layer can log it, show a user-safe message, or crash loudly during development.
class ValidationError extends Error {} function saveName(name) { try { validateName(name); return "Saved"; } catch (error) { if (!(error instanceof ValidationError)) { throw error; } return "Please enter a name."; }}This lesson keeps ValidationError minimal so the pattern is visible. The next lesson, Throwing & custom errors, covers throw, extending Error, and cause in depth.
finally and return
STEP THROUGHfinally runs while JavaScript is leaving the try statement. That includes normal completion, a caught error, an uncaught error, and a return. The powerful part is also the dangerous part: a return or throw inside finally replaces what was already happening.
Predict each puzzle before stepping. finally always runs; a return or throw inside it can replace what was already happening.
script
function returnFromTry() { try { return "try value"; } finally { return "finally value"; }} function throwThenFinallyThrows() { try { throw new TypeError("try failed"); } finally { throw new RangeError("cleanup failed"); }} function returnStillCleansUp() { const steps = []; try { steps.push("try"); return steps.join(" → "); } finally { steps.push("finally"); console.log(steps.join(" → ")); }} try { throwThenFinallyThrows();} catch (error) { console.log(error.name + ": " + error.message);}console.log(returnStillCleansUp());Use finally for cleanup: close a resource, release a lock, stop a spinner, restore temporary state. Avoid returning from it. Avoid throwing from it unless that cleanup failure is truly the error you want callers to see.
What try/catch cannot catch
A catch block protects code that runs while the matching try block is active. Two common surprises fall outside that window.
| Situation | Will this catch run? | Why |
|---|---|---|
JSON.parse(badText) inside try | Yes | The runtime throw happens while the try block is active. |
| A syntax mistake in the same script | No | The script fails before any statement runs. |
A setTimeout callback throws later | No | The callback runs in a later task after this try finished. |
The timer callback has its own try/catch | Yes | That later try is active when the throw happens. |
try { setTimeout(() => { try { throw new Error("late"); } catch (error) { console.log("callback caught itself"); } }, 0);} catch (error) { console.log("outer catch ran");}console.log("try finished");The code prints try finished, then the callback catches its own Error. Stage 6 covers promises and async/await, where asynchronous error handling gets its own rules.
- No statement in
trythrows. - Line 10 throws
TypeErrorwhile the function is running. - The same script contains
try {with a missing closing brace. - A
setTimeoutcallback throws aftertryhas already finished. trythrowsRangeError, andcatchreturns a fallback.trystarts to return a value.JSON.parsesucceeds insidetry.- The timer callback has its own
try/catcharound the risky line.
Sort each scenario by the blocks that get a chance to run.
Where you will use this
The best try/catch blocks are small. Put only the risky operation in try, handle the known failure, and let the rest of the function stay normal.
- Parsing: turn invalid JSON into a default instead of crashing a settings screen.
- Validation: show a friendly message for expected bad input, then rethrow unknown bugs.
- Cleanup: reset UI state in
finallyafter a risky operation finishes. - Debugging: log
name,message, and a stack when you cannot recover locally.
Exercise 2 asks you to write this for real: parseOrDefault(text, fallback) should return parsed data when parsing succeeds and the fallback when parsing throws. Because every parse error is handled the same way, catch { ... } is appropriate there.
Common misconceptions
“Catch fixes the error automatically.”
No. It gives you a chance to respond. You still decide whether to use a fallback, show a message, log, or rethrow.
“Finally runs only after catch.”
It runs after the try attempt whether catch ran or not, and even while a return or throw is leaving.
“A try around setTimeout catches the callback later.”
The callback runs after the outer try has finished. Put a new try/catch inside the callback, or use the asynchronous patterns you will learn in Stage 6.
“Every thrown value is an Error.”
Most built-in failures are Error objects, but JavaScript allows throwing any value. Check before reading properties.
“Returning from finally is a tidy cleanup pattern.”
It is usually a bug because it replaces a return or error from the try block. Clean up without returning.
Practice: catching the right failures
5 EXERCISESTrace the program and enter the exact printed line.
const events = [];
try {
events.push("try");
throw new TypeError("boom");
} catch (error) {
events.push(error.name);
} finally {
events.push("finally");
}
console.log(events.join(" > "));The try block records try, then throws. Catch records the error name, and finally records cleanup, so the printed path is try > TypeError > finally.
Write parseOrDefault(text, fallback). It should return parsed JSON, or fallback when parsing fails.
function parseOrDefault(text, fallback) {
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
console.log(parseOrDefault("{\"count\":2}", { count: 0 }).count);
console.log(parseOrDefault("broken", { count: 0 }).count);function parseOrDefault(text, fallback) {
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
console.log(parseOrDefault("{\"count\":2}", { count: 0 }).count);
console.log(parseOrDefault("broken", { count: 0 }).count);Valid JSON returns the parsed object, so .count is 2. Broken JSON throws a SyntaxError at runtime, catch returns the fallback, and .count is 0.
The starter should return "Ada", but finally hides it. Change cleanup so the original return survives.
function chooseName() {
try {
return "Ada";
} finally {
return "Grace";
}
}function chooseName() {
try {
return "Ada";
} finally {
console.log("cleanup");
}
}
console.log(chooseName());The fixed finally logs cleanup but does not return. The original return "Ada" is delivered to the caller.
Keep the friendly validation fallback, but do not swallow unexpected errors.
class ValidationError extends Error {}
function readAge(value) {
try {
const age = Number(value);
if (Number.isNaN(age)) throw new ValidationError("Age must be a number");
if (age < 0) throw new RangeError("Age cannot be negative");
return "Age: " + age;
} catch (error) {
if (!(error instanceof ValidationError)) throw error;
return "Use digits only.";
}
}
console.log(readAge("ten"));class ValidationError extends Error {}
function readAge(value) {
try {
const age = Number(value);
if (Number.isNaN(age)) throw new ValidationError("Age must be a number");
if (age < 0) throw new RangeError("Age cannot be negative");
return "Age: " + age;
} catch (error) {
if (!(error instanceof ValidationError)) throw error;
return "Use digits only.";
}
}
console.log(readAge("ten"));readAge("ten") throws the expected ValidationError and returns a friendly message. A negative age throws RangeError, which this catch rethrows instead of hiding.
Why does the outer catch not handle the late throw? Type the catch that does handle it.
try {
setTimeout(() => {
try {
throw new Error("late");
} catch {
console.log("callback caught itself");
}
}, 0);
} catch {
console.log("outer catch");
}
console.log("try finished");The outer try only schedules the timer and finishes. Later, the callback runs in a new task. Its own try/catch is active then, so the callback catch handles the late error.
Quiz: check your understanding
7 QUESTIONSAnswer once from prediction, then read the explanation even when you were right.
Question 1 of 7When does a
catchblock run?Choose an answer to see the explanation.
Question 2 of 7What does the successful try/finally example print?
Read the code, then predicttry { console.log("A"); } catch { console.log("B"); } finally { console.log("C"); }Choose an answer to see the explanation.
Question 3 of 7Which property usually gives the error type, such as
TypeError?Choose an answer to see the explanation.
Question 4 of 7What does the finally return puzzle print?
Read the code, then predictfunction puzzle() { try { return "try"; } finally { return "finally"; } } console.log(puzzle());Choose an answer to see the explanation.
Question 5 of 7When is optional catch binding (
catch { ... }) a good fit?Choose an answer to see the explanation.
Question 6 of 7Why rethrow an error from catch?
Choose an answer to see the explanation.
Question 7 of 7What does this print first?
Read the code, then predicttry { setTimeout(() => console.log("timer"), 0); } catch { console.log("caught"); } console.log("done");Choose an answer to see the explanation.
Key takeaways
tryprotects runtime work;catchhandles a thrown value from that work.finallyruns before leaving the try statement, even for returns and throws.- Error objects usually expose
name,message,stack, and sometimescause. - Use optional catch binding only when you truly ignore the thrown value.
- Rethrow errors your catch block cannot responsibly handle.
- A try block cannot catch syntax mistakes in the same script or errors thrown later in a timer callback.
Remember the one-liner.
A try block is protected work, catch is recovery, and finally is cleanup that runs on the way out.
Up next: Throwing & custom errors.