throw, try, catch and finally
Raise errors with throw, handle them with try/catch/finally, and know the built-in error types, optional catch binding and error causes.
What you will learn
- Raise an error with throw new Error()
- Catch and inspect errors with try, catch and finally
- Recognise the common built-in error types
- Use optional catch binding and the cause option
On this page
Things go wrong at run time: input is malformed, a value is missing, a file is not there. The fundamentals lesson on errors and exceptions explains the idea of raising an error and handling it somewhere else. This lesson covers the JavaScript syntax and the built-in error types.
throw and catch
throw stops the code that is running and hands an error to the nearest enclosing try block that has a catch. If there is none, the program stops with an error message. In the next module you will throw from inside functions, and the same rules apply there.
const divisor = 0;
try { if (divisor === 0) { throw new Error("cannot divide by zero"); } console.log(10 / divisor); console.log("not reached");} catch (error) { console.log(error.name); console.log(error.message); console.log(error instanceof Error);}
console.log("the program carries on");Errorcannot divide by zerotruethe program carries onBecause divisor is 0, the throw runs, so control jumps straight to catch, and the lines after it inside try are skipped. The caught value has a name and a message. The expression error instanceof Error asks whether error was created by Error, and here it was. After the catch block finishes, the program continues normally. Set divisor to 2 and the try block runs to the end, with no catch.
Always throw an Error object, created with new Error(message). It records a stack trace, the list of calls that led to the error, which is what you need when debugging. The next lesson shows how to read one.
The built-in error types
Error is the base type. The language itself throws a few specific subtypes, and you can throw them too, to say what kind of problem it was.
| Type | Meaning | Example |
|---|---|---|
ReferenceError |
A name that does not exist, or the temporal dead zone | undefinedVariable |
TypeError |
A value of the wrong type for the operation | null.length, assigning to a const |
RangeError |
A value outside the allowed range | new Array(-1), (1.5).toFixed(101) |
SyntaxError |
Code or data that cannot be parsed | JSON.parse("{bad json") |
URIError |
Bad input to the URI functions | decodeURIComponent("%") |
Each of the first three fails in its own way, and the name and message properties tell them apart:
try { undefinedVariable;} catch (error) { console.log(`${error.name}: ${error.message}`);}
try { null.length;} catch (error) { console.log(`${error.name}: ${error.message}`);}
try { new Array(-1);} catch (error) { console.log(`${error.name}: ${error.message}`);}ReferenceError: undefinedVariable is not definedTypeError: Cannot read properties of null (reading 'length')RangeError: Invalid array lengthRead the message first. “Cannot read properties of null (reading ‘length’)” names both the bad value and what you tried to do with it. The exact wording of engine messages can differ between JavaScript engines and versions, so match on name or instanceof in code, never on message text.
The other two types in the table come from built-in tools you meet later. JSON.parse reads text as data in the JSON format, and decodeURIComponent decodes web addresses.
try { JSON.parse("{bad json");} catch (error) { console.log(error.name);}
try { decodeURIComponent("%");} catch (error) { console.log(error.name);}SyntaxErrorURIErrorIn your own code, use TypeError for a wrong kind of input and RangeError for a value out of bounds, and plain Error for everything else. Making your own error classes comes after the lessons on classes.
finally
A finally block runs after the try and catch blocks, whether or not an error happened. Use it for cleanup that must always happen, such as closing a connection.
try { console.log("parsing"); JSON.parse("{oops"); console.log("parsed");} catch { console.log("invalid JSON");} finally { console.log("done");}parsinginvalid JSONdoneThe catch { has no (error) after it. That is optional catch binding, available since ES2019. Leave the name out when the handler does not need the error, instead of declaring an unused variable.
finally also runs when nothing went wrong:
try { console.log("working");} finally { console.log("cleanup always runs");}workingcleanup always runsfinally runs even if the try block leaves a function with return. Avoid putting a return inside finally itself: it would replace the value that try or catch was about to return. Functions and return are covered in the next module.
Adding context with cause
Sometimes you catch a low-level error and want to throw a more helpful one while keeping the original. Pass the original as the cause option (ES2022).
try { try { JSON.parse("{nope"); } catch (error) { throw new Error("could not load settings", { cause: error }); }} catch (error) { console.log(error.message); console.log(error.cause.name); console.log(error.cause instanceof SyntaxError);}could not load settingsSyntaxErrortrueThe inner catch replaces the low-level error with one that says what the program was doing (“could not load settings”), and error.cause still holds the original SyntaxError with its details. Tools that print errors usually show the cause too.
Common pitfalls
- Throwing strings or other plain values. JavaScript allows
throw "oops", but then there is no stack trace and nonameormessage. See the example after this list. - Catching everything and ignoring it. An empty
catchhides bugs. At least log the error, or rethrow what you cannot handle withthrow error. - Wrapping large blocks in
try. Put only the lines that can fail inside it, so acatchdoes not swallow unrelated mistakes. - Matching on message text. Messages differ between engines and versions. Check
error.nameorerror instanceof TypeError. - Using exceptions for normal flow. A missing item in a list is often an ordinary result, not an error. Use
undefinedornullfor it. - Putting
returninfinally. It overrides the value fromtryandcatch.
The first pitfall in code:
try { throw "just a string";} catch (caught) { console.log(typeof caught, caught instanceof Error, caught.stack);}
try { throw new Error("a real error");} catch (caught) { console.log(typeof caught, caught instanceof Error, typeof caught.stack);}string false undefinedobject true stringThe thrown string has no stack, while the real error does. Always throw Error objects.
Exercises
Declare const input = "abc";. Inside a try block, throw a TypeError if input is not a string, and a RangeError if the text is empty after trimming or does not make a number. Otherwise print the number. In catch, print NAME: MESSAGE. Then try "42", " 3.5 ", "" and 7.
Input: none. Expected output for "abc": RangeError: not a number: "abc". For the others: 42, 3.5, RangeError: empty input, TypeError: expected a string, got number.
Show solution
const input = "abc";
try { if (typeof input !== "string") { throw new TypeError(`expected a string, got ${typeof input}`); } const text = input.trim(); if (text === "") { throw new RangeError("empty input"); } const value = Number(text); if (Number.isNaN(value)) { throw new RangeError(`not a number: "${text}"`); } console.log(value);} catch (error) { console.log(`${error.name}: ${error.message}`);}RangeError: not a number: "abc"The checks go from cheap to expensive, and each throws the error type that fits. Number("abc") gives NaN rather than throwing, so the program checks for that itself. One catch at the end turns every failure into the same kind of message.
Work out the order in which this program prints its lines, then run it.
console.log("start");try { console.log("in try"); throw new Error("boom");} catch (error) { console.log("in catch:", error.message);} finally { console.log("in finally");}console.log("end");Input: none. Expected output: start, in try, in catch: boom, in finally, end.
Show solution
console.log("start");try { console.log("in try"); throw new Error("boom");} catch (error) { console.log("in catch:", error.message);} finally { console.log("in finally");}console.log("end");startin tryin catch: boomin finallyendThe throw skips the rest of the try block, catch handles the error, finally runs last, and the program then carries on with end.
Declare const age = -5;. Inside try, throw a RangeError with the message age must be between 0 and 120 when age is below 0 or above 120. Otherwise print age is OK. In catch, print NAME: MESSAGE.
Input: none. Expected output for -5: RangeError: age must be between 0 and 120. For 30: age is OK.
Show solution
const age = -5;
try { if (age < 0 || age > 120) { throw new RangeError("age must be between 0 and 120"); } console.log("age is OK");} catch (error) { console.log(`${error.name}: ${error.message}`);}RangeError: age must be between 0 and 120RangeError is the right type, because the value has the correct type but lies outside the allowed range. Change age to 30 and the catch block is skipped.
Summary
throw new Error(message)raises an error.try/catchhandles it, andfinallyalways runs.- Throw
Errorobjects, not strings. They carry aname, amessageand a stack trace. - The language throws
ReferenceError,TypeError,RangeError,SyntaxErrorandURIError. Match on the type, not the message text. catch {without a binding is allowed when you do not need the error. Pass{ cause }to keep the original error when you wrap it.
Next: finding bugs with the console, stack traces and a debugger, in the debugging lesson.