Errors & Exceptions
Every program meets problems: a user types letters into a number box, a server is down, saved data is broken. When JavaScript cannot carry on, it throws an error. If nothing catches it, the current code stops and an error appears in the console. Good programs expect these moments and respond calmly: they show a helpful message, keep the rest of the page working and record what went wrong.
In this lesson you will read JavaScript's built-in error types, catch errors with try...catch...finally, throw your own, attach the original error with cause, build custom error classes and learn where Error.isError() from ES2026 helps.
Built-in error types
There are two broad kinds of error. A syntax error means the code itself is not valid JavaScript, such as a missing bracket. The browser cannot run the file at all, and no try...catch inside it can help. A runtime error, also called an exception, happens while valid code runs. Those are the ones you can catch.
Each runtime error is an object with a name and a message:
| Error type | Typical cause |
|---|---|
ReferenceError | Using a variable that does not exist |
TypeError | Using a value the wrong way, such as reading a property of null or calling something that is not a function |
RangeError | A number outside the allowed range |
SyntaxError | Invalid text given to a parser at run time, such as JSON.parse() |
AggregateError | Several errors at once, for example from Promise.any() |
const attempts = [
() => missingVariable + 1,
() => null.length,
() => (12.5).toFixed(200),
() => JSON.parse("{bad json}"),
];
for (const attempt of attempts) {
try {
attempt();
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}
}
console.log("The program is still running.");ReferenceError: missingVariable is not defined TypeError: Cannot read properties of null (reading 'length') RangeError: toFixed() digits argument must be between 0 and 100 SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2) The program is still running.
The output above comes from Chrome. Other browsers use the same error names but word some messages differently, so your code should check name or the error type, never the exact message text.
try, catch and finally
Put code that might fail inside try. If anything in it throws, JavaScript jumps straight to catch, which receives the error object. Code in finally runs at the end either way, whether the try succeeded, failed, or even returned early. That makes finally the right place for clean-up such as hiding a loading message or re-enabling a button.
Errors also have a stack property: a list of the function calls that led to the error, with file names and line numbers. It is very useful in the console and in error logs, but not for showing to users.
If you do not need the error object, you can leave out the brackets: catch { ... }.
Throwing your own errors
Use throw when your function receives something it cannot work with. Throw an Error object, ideally the type that fits best, with a message that says what is wrong:
function payFare(wallet, fare) {
if (typeof fare !== "number" || Number.isNaN(fare)) {
throw new TypeError(`Fare must be a number, got ${typeof fare}`);
}
if (fare > wallet.balance) {
throw new RangeError(`Need ₹${fare} but only ₹${wallet.balance} left`);
}
wallet.balance -= fare;
return wallet.balance;
}
const wallet = { owner: "Farhan", balance: 120 };
for (const fare of [45, "60", 100]) {
try {
const left = payFare(wallet, fare);
console.log(`Paid ₹${fare}, ₹${left} left`);
} catch (error) {
console.log(`${error.name}: ${error.message}`);
} finally {
console.log("Ticket counter ready for the next passenger");
}
}Paid ₹45, ₹75 left Ticket counter ready for the next passenger TypeError: Fare must be a number, got string Ticket counter ready for the next passenger RangeError: Need ₹100 but only ₹75 left Ticket counter ready for the next passenger
Checking inputs at the start of a function and throwing early is called failing fast. The error appears right where the problem is, instead of three functions later as a confusing NaN.
JavaScript lets you write throw "Something broke", but do not. A plain string has no name and no stack, so you lose the most useful debugging information.
Custom errors and cause
Sometimes you need to tell different problems apart. A user who typed a short mobile number needs a friendly hint next to that field; a broken file is a different story. A custom error class extends Error (see Inheritance & Prototypes), sets its name and can carry extra data, such as which field was wrong.
When you catch a low-level error and throw a clearer one, pass the original as cause: new Error("message", { cause: error }). The new message makes sense to the rest of your app, and the original details are kept for debugging.
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function readProfile(text) {
let data;
try {
data = JSON.parse(text);
} catch (error) {
throw new Error("Could not read the saved profile", { cause: error });
}
if (!/^[6-9]\d{9}$/.test(data.mobile ?? "")) {
throw new ValidationError("mobile", "Enter a 10-digit mobile number");
}
if (!(data.age >= 18)) {
throw new ValidationError("age", "You must be 18 or older");
}
return data;
}
const inputs = [
'{"name":"Meera","mobile":"9876543210","age":24}',
'{"name":"Rohan","mobile":"12345","age":30}',
'{"name":"Isha","mobile":"9812345678","age":16}',
'{"name": broken',
];
for (const text of inputs) {
try {
const profile = readProfile(text);
console.log(`Welcome, ${profile.name}`);
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Please fix "${error.field}": ${error.message}`);
} else {
console.log(`${error.message} (cause: ${error.cause?.name})`);
}
}
}Welcome, Meera Please fix "mobile": Enter a 10-digit mobile number Please fix "age": You must be 18 or older Could not read the saved profile (cause: SyntaxError)
Inside the custom class, super(message) must come first so Error can set up the message and stack. The instanceof check then lets the catch block treat expected validation problems differently from real failures. The mobile number check uses a regular expression, which you will learn in Regular Expressions.
Errors in asynchronous code
With async/await, a rejected promise throws at the await, so an ordinary try...catch works (see async & await). Without await, the rejection happens later, after the try block has finished, and escapes:
try {
fetch("/api/profile"); // no await: a failure here is NOT caught below
} catch (error) {
showMessage("Could not load profile");
}As a last line of defence, you can log anything that slips through with two global listeners. They are for recording problems, for example by sending them to your server, not a replacement for proper handling:
window.addEventListener("error", (event) => {
console.log("Uncaught error:", event.error);
});
window.addEventListener("unhandledrejection", (event) => {
console.log("Unhandled promise rejection:", event.reason);
});Common mistakes
- Empty
catchblocks.catch {}hides bugs completely. At least log the error, or let it go. - Catching everything. Catch the errors you can actually handle and re-throw the rest with
throw error. - Throwing strings. Always throw
Errorobjects so you keepnameandstack. - Checking
error.messagetext. Messages change between browsers and versions. Check the type orname. - Forgetting
super(message)orthis.namein a custom error. The message or the type name will be missing.
Key points
- Syntax errors stop a file from running at all; runtime errors (exceptions) can be caught.
tryholds risky code,catchhandles a thrown error, andfinallyalways runs for clean-up.- Throw
Errorobjects of a fitting type, early, with clear messages. - Use
new Error(message, { cause })to wrap a low-level error without losing it. - Custom classes that extend
Errorlet you tell expected problems apart withinstanceof. Error.isError()(ES2026) reliably recognises real error objects, even across iframes.
Exercise
Write a function parseQuantity(text) for a shop's quantity box. It should throw a TypeError if the text is not a whole number, and a RangeError if the number is less than 1 or more than 10. Test it with "3", "abc" and "25" inside one loop, logging either "Quantity: 3" or the error's name and message, and log "Checked" in a finally block each time.
Show solution
function parseQuantity(text) {
const qty = Number(text);
if (!Number.isInteger(qty)) {
throw new TypeError(`"${text}" is not a whole number`);
}
if (qty < 1 || qty > 10) {
throw new RangeError(`Quantity must be 1 to 10, got ${qty}`);
}
return qty;
}
for (const input of ["3", "abc", "25"]) {
try {
console.log(`Quantity: ${parseQuantity(input)}`);
} catch (error) {
console.log(`${error.name}: ${error.message}`);
} finally {
console.log("Checked");
}
}Number("abc") is NaN, which is not an integer, so the first check throws a TypeError. The finally block runs after every attempt, whether it succeeded or failed.
Quick Quiz
1. When does the finally block run?
finally always runs, which makes it ideal for clean-up work.
2. Why should you throw new Error("...") instead of a plain string?
A thrown string works, but it has no stack trace or type, so you lose valuable information.
3. What is the cause option used for?
new Error("Could not load", { cause: error }) keeps the low-level error available for debugging.
4. Why can Error.isError() be more reliable than instanceof Error?
Errors from an iframe use a different Error class, so instanceof says false, but Error.isError() still says true.
Learn JavaScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
