try/catch and Custom Errors
Use try, catch and finally, catch only what you can handle, write your own error classes, and catch errors from async code. After this, expected problems get handled and real bugs still show up.
Course content
Sign in to save your progress.
-
Set Up Projects That Don't Break Each Other
-
Node.js and npm: Running JavaScript Outside the Browser
-
package.json and Lock Files: Same Install Every Time
-
-
-
Organize Code Other People Can Read
-
JavaScript Modules: import, export and Splitting One Big File
-
JavaScript Classes: When an Object Literal Is No Longer Enough
-
Destructuring and Spread Without the Confusion
-
-
-
Work With Real Data
-
Array Methods: map, filter, reduce and When a Loop Is Clearer
-
Working With JSON: parse, stringify and What Breaks
-
Dates in JavaScript Without Losing a Day
-
-
-
Waiting for Things
-
Callbacks to Promises: Why the Code Runs Out of Order
-
async/await and fetch: Calling a Real API
-
-
-
Prove It Works Before Someone Else Finds Out
-
try/catch and Custom Errors
-
Your First Tests With Vitest
-
-
- Final Check: JavaScript Intermediate
Skip ahead: If you already write your own error classes and check them with instanceof, skip to "Custom error classes".
Errors aren't the enemy. An error that stops the program with a clear message is the second-best outcome after no error. The worst outcome is an error that gets swallowed, so the program keeps going with bad data and nobody knows. try/catch is how you decide which errors you can actually handle, and custom errors are how you tell them apart.
try, catch, finally
function parseQuantity(text) {
const qty = Number(text);
if (!Number.isInteger(qty) || qty < 1) {
throw new RangeError(`Quantity must be a whole number of at least 1, got "${text}"`);
}
return qty;
}
for (const input of ["3", "abc"]) {
try {
console.log("quantity:", parseQuantity(input));
} catch (error) {
console.log(`${error.name}: ${error.message}`);
} finally {
console.log("checked", input);
}
}
quantity: 3
checked 3
RangeError: Quantity must be a whole number of at least 1, got "abc"
checked abc
tryruns the code.catchruns only if something intrythrew.erroris whatever was thrown.finallyruns either way. Use it for cleanup: hide a spinner, close a file, re-enable a button.
Always throw an Error (or a subclass), never a string. throw "bad quantity" has no stack trace and no name, so whoever catches it can't tell where it came from.
Catch only what you can handle
Here's the pattern that hides bugs:
try {
saveOrder(order);
} catch (error) {
// ignore
}
If saveOrder fails because the network is down, fine, maybe you can retry. But if it fails because of a typo in your code, this hides it too. The order isn't saved, the user thinks it is, and the logs are empty.
Ask two questions before writing a catch:
- Which errors do I expect here?
- What will I actually do about them: retry, show a message, use a default?
If the answer to the second question is "nothing", don't catch. Let it go up to code that can do something, or let it crash loudly.
⚠ Warning: Never leave a catch block empty. At the very least log the error with context: console.error("saving order 1042 failed", error). An empty catch turns a five-minute bug into a two-day investigation.
Custom error classes
To handle some errors and let the rest through, you need to tell them apart. Checking the message text is fragile, since someone will reword it. Make your own error class instead:
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function checkOrder(order) {
if (!order.email?.includes("@")) {
throw new ValidationError("email", "Enter a valid email address");
}
if (order.items.length === 0) {
throw new ValidationError("items", "Add at least one item");
}
}
function submit(order) {
try {
checkOrder(order);
console.log("order OK");
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Show next to ${error.field}: ${error.message}`);
} else {
throw error;
}
}
}
submit({ email: "ana@example.com", items: ["rice"] });
submit({ email: "ana", items: ["rice"] });
submit({ email: "ana@example.com", items: [] });
order OK
Show next to email: Enter a valid email address
Show next to items: Add at least one item
The important line is throw error in the else. Validation errors are expected, so they're handled. Anything else is a real bug, so it's thrown again instead of being treated as bad input.
extends Errorgives you the message and the stack trace.this.namemakes logs showValidationErrorinstead ofError.- Extra fields like
fieldcarry what the handler needs.
The tutorial Custom Error Classes You Can Actually Catch adds an HTTP error class and the cause option for wrapping errors.
Errors in async code
try/catch works around await exactly like synchronous code. It does not catch errors from a promise you didn't await:
async function failLater() {
throw new Error("failed after waiting");
}
try {
failLater();
} catch (error) {
console.log("never runs");
}
console.log("try block finished");
try block finished
...
Error: failed after waiting
...
The try finished before the promise rejected, so the catch never saw it. Node printed the error and stopped the program with exit code 1. Add await in front of failLater() and the catch gets it.
💡 Tip: When you rethrow with more context, keep the original: throw new Error("Could not save order 1042", { cause: error }). Node and the browser console show both, so you don't lose the real reason.
Common mistakes
- Empty
catchblocks. - Catching everything and showing "Something went wrong" for a typo in your own code.
- Throwing strings.
- Checking
error.message === "..."instead of the error class. - Forgetting
awaitinsidetry.
What happens if you skip this
Your app will either crash on every bad input, or, after someone "fixes" that with a big try/catch, silently do the wrong thing. The second is worse. Crashes get reported. Silent failures get discovered by customers.
Find one catch block in your code. If it caught a TypeError from a typo, would anyone ever find out?
About
Use try, catch and finally, catch only what you can handle, write your own error classes, and catch errors from async code. After this, expected problems get handled and real bugs still show up.
- Duration
- 5 min
- Last update
- 09/08/2026
Comments (0)
Sign in to commentNo comments yet.