JavaScript course menu
JavaScript Error Handling: try, catch, finally, throw and Custom Errors
Learn error handling in JavaScript: built-in error types, try, catch and finally, throwing your own errors, custom error classes, the cause option, rethrowing, handling async errors, and global error handlers, with best practices.
On this page
Things go wrong: a network request fails, a user types letters into a number field, a file is missing. Error handling lets your program respond sensibly instead of crashing or showing a blank page.
Built-in errors#
When JavaScript hits a problem it throws an error object. Common types:
Every error has a name, a message, and a stack (the trace of function calls, very useful for debugging).
try...catch#
Code that might fail goes in try; if anything inside throws, execution jumps to catch:
const user = undefined;
try {
console.log("Before the problem");
console.log(user.name);
console.log("This line never runs");
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}
console.log("The program keeps going");Before the problem
TypeError: Cannot read properties of undefined (reading 'name')
The program keeps goingIf you do not need the error object, you can write just catch { ... }.
finally#
finally runs whether or not an error happened, even after a return. It is used for cleanup such as hiding a loading spinner or closing a file:
function parsePrice(text) {
try {
const value = JSON.parse(text);
return `price is ${value.price}`;
} catch {
return "invalid data";
} finally {
console.log(`(finished parsing ${text})`);
}
}
console.log(parsePrice('{"price": 499}'));
console.log(parsePrice("oops"));(finished parsing {"price": 499})
price is 499
(finished parsing oops)
invalid dataThrowing your own errors#
Use throw to signal that something is wrong. Always throw Error objects (not strings), so you get a stack trace:
function withdraw(balance, amount) {
if (typeof amount !== "number" || Number.isNaN(amount)) {
throw new TypeError("Amount must be a number");
}
if (amount > balance) {
throw new RangeError(`Cannot withdraw ₹${amount}; balance is ₹${balance}`);
}
return balance - amount;
}
for (const amount of [200, 5000, "100"]) {
try {
console.log("New balance:", withdraw(1000, amount));
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}
}New balance: 800
RangeError: Cannot withdraw ₹5000; balance is ₹1000
TypeError: Amount must be a numberCustom error classes#
For your own kinds of failure, extend Error. Code can then react differently to each type with instanceof:
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function register({ email, age }) {
if (!email.includes("@")) throw new ValidationError("email", "Email is not valid");
if (age < 13) throw new ValidationError("age", "You must be at least 13");
return "Registered!";
}
try {
register({ email: "asha@example.com", age: 10 });
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Problem with ${error.field}: ${error.message}`);
} else {
throw error; // not ours: let it go up
}
}Problem with age: You must be at least 13Rethrowing errors you do not know how to handle is important: swallowing every error silently hides real bugs. Classes are covered in Classes.
Wrapping errors with cause#
When you catch a low-level error and throw a more meaningful one, keep the original as the cause:
function loadConfig(text) {
try {
return JSON.parse(text);
} catch (error) {
throw new Error("Could not load settings file", { cause: error });
}
}
try {
loadConfig("{bad json");
} catch (error) {
console.log(error.message);
console.log("Caused by:", error.cause.name);
}Could not load settings file
Caused by: SyntaxErrorAsync errors#
With async/await, rejected promises are thrown at the await, so try...catch works the same way (async and await). With .then() chains, use .catch().
try...catch does not catch errors thrown later inside a callback, such as a timer, because the try block has already finished by then. Handle errors inside the callback itself.
Global handlers#
As a last resort, you can log errors that nothing caught, for example to send them to an error-tracking service:
// Browsers
window.addEventListener("error", (event) => report(event.error));
window.addEventListener("unhandledrejection", (event) => report(event.reason));
// Node.js
process.on("uncaughtException", (error) => { report(error); process.exit(1); });These are for reporting, not for recovering; fix the cause instead.
Best practices#
- Catch errors where you can do something useful: show a message, retry, use a fallback.
- Show users friendly messages and log technical details for developers.
- Never leave an empty
catch {}that hides failures, unless ignoring the error is truly intended (and commented). - Validate input early and throw clear, specific errors.
Practice#
Practice exercise
Write safeDivide(a, b) that throws a TypeError if either argument is not a number and a RangeError if b is zero, and otherwise returns a / b. Call it with (10, 2), (5, 0) and (4, "2") inside try...catch, printing either the result or the error name and message.
Show solution
function safeDivide(a, b) {
if (typeof a !== "number" || typeof b !== "number") {
throw new TypeError("Both arguments must be numbers");
}
if (b === 0) {
throw new RangeError("Cannot divide by zero");
}
return a / b;
}
for (const [a, b] of [[10, 2], [5, 0], [4, "2"]]) {
try {
console.log(safeDivide(a, b));
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}
}5
RangeError: Cannot divide by zero
TypeError: Both arguments must be numbersKey takeaway#
Finished this lesson?