throw, try, catch and finally

Beginner35 min

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.

throw_catch.mjs
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");
Output
Error
cannot divide by zero
true
the program carries on

Because 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:

error_messages.mjs
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}`);
}
Output
ReferenceError: undefinedVariable is not defined
TypeError: Cannot read properties of null (reading 'length')
RangeError: Invalid array length

Read 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.

error_types_more.mjs
try {
JSON.parse("{bad json");
} catch (error) {
console.log(error.name);
}
try {
decodeURIComponent("%");
} catch (error) {
console.log(error.name);
}
Output
SyntaxError
URIError

In 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.

finally_demo.mjs
try {
console.log("parsing");
JSON.parse("{oops");
console.log("parsed");
} catch {
console.log("invalid JSON");
} finally {
console.log("done");
}
Output
parsing
invalid JSON
done

The 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:

finally_ok.mjs
try {
console.log("working");
} finally {
console.log("cleanup always runs");
}
Output
working
cleanup always runs

finally 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).

error_cause.mjs
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);
}
Output
could not load settings
SyntaxError
true

The 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 no name or message. See the example after this list.
  • Catching everything and ignoring it. An empty catch hides bugs. At least log the error, or rethrow what you cannot handle with throw error.
  • Wrapping large blocks in try. Put only the lines that can fail inside it, so a catch does not swallow unrelated mistakes.
  • Matching on message text. Messages differ between engines and versions. Check error.name or error instanceof TypeError.
  • Using exceptions for normal flow. A missing item in a list is often an ordinary result, not an error. Use undefined or null for it.
  • Putting return in finally. It overrides the value from try and catch.

The first pitfall in code:

throw_values.mjs
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);
}
Output
string false undefined
object true string

The thrown string has no stack, while the real error does. Always throw Error objects.

Exercises

ExerciseA robust input-to-number check

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
to_number.mjs
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}`);
}
Output
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.

ExercisePredict the order

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
finally_order.mjs
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");
Output
start
in try
in catch: boom
in finally
end

The throw skips the rest of the try block, catch handles the error, finally runs last, and the program then carries on with end.

ExerciseValidate an age

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
validate_age.mjs
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}`);
}
Output
RangeError: age must be between 0 and 120

RangeError 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/catch handles it, and finally always runs.
  • Throw Error objects, not strings. They carry a name, a message and a stack trace.
  • The language throws ReferenceError, TypeError, RangeError, SyntaxError and URIError. 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.