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.
- 01Follow the three blocksSay which of
try,catch, andfinallyrun, and in what order. - 02Read Error objectsUse
name,message,stack, andcauseon a caught error, and check it withinstanceof. - 03Handle only what you understandLeave out the catch name when you don't need it, throw again the errors you can't deal with, and keep
finallyfor cleanup.
Catching an error on purpose
JavaScript runs your code one statement after another. Sometimes a statement fails. Then JavaScript throws an error: it stops the current work and hands the error back to the code that called it. If no code deals with the error, the program stops and the console shows a red message.
You learned to read those messages in Reading error messages. In The call stack, you saw an error leave one function call after another. This lesson shows how to deal with an error on purpose, so your program can keep going. You use three blocks: try, catch, and finally.
A try statement has up to three blocks:
tryholds the code that might fail.catchruns only if that code throws. It gets the error, so you can deal with it.finallyruns at the end, whether there was an error or not. You use it to clean up.
You stand on a ladder to change a light bulb. There is a safety net under the ladder. If you slip, the net catches you, and you are fine. When you are done, you put the ladder away. You do that whether you slipped or not.
- In real life: Changing a light bulb on a ladder
- In JavaScript: The code inside
try - In real life: Slipping off the ladder
- In JavaScript: An error is thrown
- In real life: The safety net catches you
- In JavaScript: The
catchblock runs - In real life: Putting the ladder away
- In JavaScript: The
finallyblock runs
Use try and catch when you know something might fail, and you know what to do if it does. For example:
- Reading text as JSON, a common text format for data. The text might be broken.
- Reading data saved in the browser. It might not be there.
- Checking a form. The user might type something wrong.
Don't wrap every line in try "just in case". A catch that hides errors you didn't expect makes bugs harder to find.
try, catch & finally
STEP THROUGHHere is the whole idea in one small program. The path array keeps a list of the blocks that ran, in order. Pick what happens inside try: it works, or it throws one of two errors. Then step through the run.
Pick what happens inside try, guess 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);First, the case where nothing goes wrong. try runs first. catch is skipped. finally still runs at the end.
try {
console.log("try");
} catch {
console.log("catch");
} finally {
console.log("finally");
}Line 2 prints try. Nothing throws, so the catch block on lines 3 and 4 never runs. Line 6 prints finally. The output is try, then finally.
Now a case where a line throws. JavaScript skips the rest of try, runs catch, and then runs finally.
try {
console.log("try");
throw new Error("sold out");
} catch (error) {
console.log("catch: " + error.message);
} finally {
console.log("finally");
}Line 2 prints try. Line 3 uses throw to throw an error on purpose. new Error("sold out") makes an error object whose message is sold out. JavaScript stops try right there and jumps to catch on line 4. The variable error now holds the thrown error, so line 5 prints catch: sold out. Then line 7 prints finally.
catch. When line 3 throws, it jumps to catch. Either way, finally runs last.- No error:
tryruns,catchis skipped, andfinallyruns. - An error:
trystarts, a line throws, JavaScript jumps tocatch, and thenfinallyruns. - Leaving early:
finallyalso runs whentryorcatchleaves early withreturnor a new error. You'll see this later in the lesson.
catch only works for errors thrown while the code runs. Some SyntaxErrors happen that way. For example, JSON.parse reads JSON text, and it throws a SyntaxError when the text is broken. A catch can deal with that.
A typing mistake in your own JavaScript file is different. Say you forget a closing brace, }. Then JavaScript can't read the file at all. No line runs, so the try never starts.
const cart = { total: 12 };
try {
if (cart.total > 10) throw new Error("Limit reached");
console.log("checkout");
} catch (error) {
console.log("show message: " + error.message);
}Line 1 makes a cart whose total is 12. On line 4, 12 is more than 10, so the if throws an error with the message Limit reached. Line 5 never runs. The catch block prints show message: Limit reached, and the page keeps working.
The built-in error types
INTERACTIVEWhen JavaScript throws an error, the thrown value is usually an Error object. It has three properties you'll use most:
nameis the kind of error, likeTypeError.messagesays what went wrong, in words.stackis text that shows where the error was made, and which function calls led there. Each browser writes it a little differently.
JavaScript has several built-in kinds of error. Read each row of the table as: this kind of error, what usually causes it, and a short example.
| Kind of error | What usually causes it | A short example |
|---|---|---|
SyntaxError | The code can't be read, or JSON text is broken. | JSON.parse("{broken}") inside try |
ReferenceError | You used a name that doesn't exist here. | Reading missingValue when it was never made |
TypeError | A value is the wrong kind for what you did with it. | null.trim(), because null has no methods |
RangeError | A number is outside the allowed range. | new Array(-1), because a list can't have -1 items |
URIError | A web address has a broken % code in it. | decodeURIComponent("%") |
EvalError | An old kind of error. Modern JavaScript almost never throws it. | You mostly see it in old documents. |
AggregateError | Several errors are reported together, as one. | Promise.any(...) makes one when every promise fails. |
Try the playground below. Each button throws a real error in your browser and catches it straight away. Then you can see the properties your browser filled in.
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 see name, message, stack, instanceof, and cause.
Pick a kind of error, then run it. The code catches the error and shows its properties from your browser.
An error can also have a cause property. You add it yourself when you make a new error because of an older one. The new error gets a clearer message. cause keeps the older error, so the first reason isn't lost.
function readSettings() {
throw new SyntaxError("bad JSON");
}
try {
readSettings();
} catch (cause) {
const error = new Error("Could not load settings", { cause });
console.log(error.message);
console.log(error.cause.name);
}Line 2 throws a SyntaxError. Line 7 catches it in a variable called cause. Line 8 makes a new error with a clearer message. The { cause } part is short for { cause: cause }: it keeps the first error inside the new one. Line 9 prints Could not load settings, and line 10 prints SyntaxError.
JavaScript lets you throw any value, not just an Error object. So check what you caught before you read message. error instanceof Error is true when error is an Error object. The Checking types reliably lesson explains instanceof and Error.isError.
try {
throw "offline";
} catch (error) {
console.log(typeof error);
console.log(error);
}Line 2 throws the text "offline". That is allowed, even though it isn't an Error object. Line 4 prints string, and line 5 prints offline. A string has no name or message, so check what you caught before you read them.
Optional catch binding
In catch (error), the name error is called the catch binding. It is the variable that holds the thrown value. Sometimes you don't need that value. You only need to know that something failed. Then you can leave the name out and write catch { ... }. This is called optional catch binding.
try { JSON.parse(settingsText);} catch { settings = defaultSettings;}Line 2 reads the settings text as JSON. If that throws, line 4 uses the default settings instead. Line 3 has no name in brackets, because every failure gets the same default.
Keep the name, catch (error), when you need the error. For example, when you want to:
- Print or log the error.
- Check
error.name. - Show a message that depends on the error.
- Throw the error again.
Leaving out the name is not always better. Do it only when you would never use the error.
Rethrowing errors you cannot handle
CODE PATTERNA catch block should only deal with errors it understands. Other errors should go on to code that can deal with them. To pass an error on, you throw it again from inside catch: throw error. This is called rethrowing.
Picture a small net under the ladder, and a bigger net below it. The small net is made for one kind of slip. If something else falls into it, the small net doesn't keep it. It drops it into the bigger net, which knows what to do.
- In real life: The small net catches the slip it was made for
- In JavaScript:
catchdeals with aValidationError - In real life: Something else falls into the small net
- In JavaScript: A different error, like a
TypeError - In real life: The small net drops it into the bigger net
- In JavaScript:
throw errorpasses it to the calling code
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."; }}Line 1 makes a new kind of error, ValidationError, for bad form input. extends Error means it is a kind of Error. Line 5 calls validateName, a function that isn't shown here. It checks the name and may throw. Line 8 asks: is this error a ValidationError? If not, line 9 throws it again. Only a ValidationError reaches line 11, which returns a friendly message.
catch answers only the error it knows, a ValidationError. Any other error is thrown again, so the caller can deal with it.This lesson keeps ValidationError as small as possible. The next lesson, Throwing & custom errors, covers throw, making your own kinds of error, and cause in more depth.
finally and return
STEP THROUGHfinally runs just before JavaScript leaves the try statement. That happens in four cases:
tryfinishes with no error.catchdeals with an error.- An error leaves, and no
catchstops it. tryorcatchrunsreturn.
There is one trap. If finally itself runs return or throw, that replaces what was already happening. The earlier return value or error is lost. Step through these three puzzles, and guess each result before you step.
Guess each puzzle before you step. finally always runs. A return or throw inside it replaces 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());function saveDraft() {
try {
return "saved";
} finally {
console.log("stop spinner");
}
}
console.log(saveDraft());Line 3 gets ready to return "saved". Before the value reaches the caller, finally runs, and line 5 prints stop spinner. A spinner is the turning icon a page shows while it waits. Then line 9 prints saved.
function chooseMessage() {
try {
return "saved";
} finally {
return "failed";
}
}
console.log(chooseMessage());Line 3 gets ready to return "saved". Then finally runs, and line 5 returns "failed" instead. The "saved" value is lost, so line 9 prints failed. This is a common mistake.
try blocks return "saved", and finally runs before the caller gets it. On the right, the return in finally replaces "saved" with "failed".Use finally only to clean up. For example:
- Close a file or a connection you opened.
- Stop a loading spinner.
- Put back a setting you changed for a moment.
Don't return from finally. Don't throw from it either, unless the cleanup error is the one you want the caller to see.
What try/catch cannot catch
A catch block only catches errors thrown while its try block is running. Two cases surprise people, because the error happens outside that time. Read each row of the table as: in this case, does this catch run, and why?
| What happens | Does this catch run? | Why |
|---|---|---|
JSON.parse(badText) is inside try. | Yes. | The error is thrown while try is running. |
| The same file has a typing mistake, like a missing brace. | No. | JavaScript can't read the file, so no line runs at all. |
A setTimeout callback throws later. | No. | The callback runs after this try has already finished. |
The timer callback has its own try/catch. | Yes. | That inner try is running when the error is thrown. |
setTimeout(fn, 0) asks JavaScript to run the function fn later, after the current code finishes. A function you hand to other code, to run later, is called a callback.
try {
setTimeout(() => {
throw new Error("late error");
}, 0);
} catch {
console.log("outer catch");
}
console.log("try finished");Line 2 only schedules the callback. It doesn't run it yet. So try finishes with no error, and line 8 prints try finished. Later, the callback runs, and line 3 throws. The outer catch is already finished, so nothing catches the error. The console shows it as an uncaught error: Error: late error.
try and catch are already finished when the callback runs. So the callback's error has no catch around it.If you wrote the callback yourself, put a try/catch inside it.
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");Line 12 prints try finished first. Later, the callback runs. Its own try on line 3 is running when line 4 throws, so the catch on line 5 deals with it. Line 6 prints callback caught itself. A later lesson on promises and async/await shows more ways to handle errors in code that runs later.
async function loadProfile() {
try {
await Promise.reject(new Error("Profile unavailable"));
} catch (error) {
console.log(error.message);
}
}
loadProfile();await is different from a timer. A promise is a value that arrives later. Promise.reject(...) makes a promise that fails with an error. On line 3, await pauses the async function until the promise is done.
When the promise fails, the function goes on from line 3 and throws the error right there, inside try. So the catch on line 4 deals with it, and line 5 prints Profile unavailable. A callback that throws later still needs its own try/catch.
- No line in
trythrows. - Line 10, inside
try, throws aTypeError. - The same file has
try {with no closing brace. - A
setTimeoutcallback throws aftertryhas finished. trythrows aRangeError, andcatchreturns a default value.tryrunsreturnwith a value.JSON.parseworks insidetry.- The timer callback has its own
try/catcharound the line that throws.
Put each case in the group that says which blocks run.
Where you will use this
Good try blocks are small. Put only the line that might fail inside try. Deal with the error you expect. Leave the rest of the function as normal code.
- Reading JSON: use a default value when the JSON text is broken, so a settings screen still opens.
- Checking input: show a friendly message when a user types something wrong. Throw any other error again.
- Cleaning up: use
finallyto reset the page after work that might fail. For example, stop a spinner. - Finding bugs: when you can't fix an error where it happens, log its
name,message, andstack.
In Exercise 2 you write this yourself. parseOrDefault(text, fallback) returns the parsed data when the text is good. When JSON.parse throws, it returns fallback, a default value instead. Every parse error gets the same default, so catch { ... } with no name fits here.
function readSettings(text) {
try {
return JSON.parse(text);
} catch {
return { theme: "light" };
}
}
console.log(readSettings("{broken}").theme);The text {broken} is not valid JSON, so line 3 throws a SyntaxError. The catch on line 4 returns a light theme instead. Line 9 prints light, and the settings screen still works.
try {
try {
JSON.parse("{broken}");
} catch {
console.log("inner fallback");
throw new Error("settings unavailable");
}
} catch (error) {
console.log("outer report: " + error.message);
}A try can sit inside another try. Line 3 throws a SyntaxError. The inner catch on line 4 deals with it: line 5 prints inner fallback, and line 6 throws a new error with a clearer message. The outer catch on line 8 gets that new error, and line 9 prints outer report: settings unavailable.
Put one try inside another only when each one has a different job.
Common mistakes
"catch fixes the error for you."
No. catch only gives you a chance to respond. You still choose what to do: use a default value, show a message, log the error, or throw it again.
"finally runs only after catch."
No. finally runs whether catch ran or not. It even runs when try leaves early with return or an error.
"A try around setTimeout catches errors in the callback."
No. The callback runs after the outer try has finished. Put a new try/catch inside the callback. A later lesson on promises and async/await shows more ways.
"Every thrown value is an Error object."
No. Most built-in errors are Error objects, but JavaScript lets you throw any value, even a string. Check what you caught before you read its properties.
"A return in finally is a tidy way to clean up."
No. It is usually a bug, because it replaces the return value or the error from try. Clean up in finally, but don't return from it.
Practice: catching the right errors
5 EXERCISESTry each exercise on your own first. Use the hints only if you get stuck.
The program adds a word to events each time a block runs. The last line joins the words, with " > " between them. Type the exact line it prints.
const events = [];
try {
events.push("try");
throw new TypeError("boom");
} catch (error) {
events.push(error.name);
} finally {
events.push("finally");
}
console.log(events.join(" > "));try adds try, then throws. catch adds the error's name, TypeError. finally adds finally. So the program prints try > TypeError > finally.
Write parseOrDefault(text, fallback). It should return the parsed JSON. If parsing fails, it should return fallback.
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);Good JSON gives back the object, so .count is 2. Broken JSON makes JSON.parse throw a SyntaxError. catch returns the fallback, so .count is 0.
The function should return "Ada", but finally replaces it. Change finally so the first return value gets through.
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 doesn't return. So the first return "Ada" reaches the caller.
Keep the friendly message for a ValidationError, but don't hide any other error.
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, so the function returns a friendly message. A negative age throws a RangeError. This catch throws it again instead of hiding it.
Why doesn't the outer catch deal with the late error? Type which catch does.
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, then finishes. Later, the callback runs. Its own try/catch is running at that moment, so the callback's catch deals with the late error.
Quiz: check your understanding
7 QUESTIONSPick an answer first. 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 this code print? Nothing in
trythrows.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 kind of error, such as
TypeError?Choose an answer to see the explanation.
Question 4 of 7What does this code print? Both
tryandfinallyreturn.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 { ... }with no name, a good choice?Choose an answer to see the explanation.
Question 6 of 7Why throw an error again 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
tryholds code that might fail.catchruns only if that code throws, and it gets the error.finallyruns just before JavaScript leaves thetrystatement, even after areturnor an error.- An Error object usually has
name,message, andstack. Some also havecause. - Write
catch { ... }with no name only when you never use the error. - Throw again any error your
catchcan't deal with properly. trycan't catch a typing mistake in the same file, or an error thrown later in a timer callback.
Remember the one-liner.try is the work that might fail, catch is what you do if it fails, and finally is the cleanup that always runs at the end.
Up next: Throwing & custom errors.