Free Online Code Editor
Learn how to catch, throw, and design errors in JavaScript, including custom error classes, finally, and the async mistakes that let failures slip through.
Every program fails sometimes. A user types something strange, a server goes down, a file is missing. Good error handling isn't about preventing every failure. It's about failing in a way that's clear, safe, and fixable.
Wrap risky code in try. If something throws, JavaScript jumps to catch. Code in finally runs either way.
function parseSettings(text) {
try {
return JSON.parse(text);
} catch (error) {
console.log("Bad settings:", error.name);
return {};
} finally {
console.log("parse attempt finished");
}
}
console.log(parseSettings('{"theme":"dark"}'));
console.log(parseSettings("{oops"));parse attempt finished
{ theme: 'dark' }
Bad settings: SyntaxError
parse attempt finished
{}finally is the right place for cleanup: closing a connection, hiding a loading spinner, releasing a lock.
Use throw when something is wrong and the current function can't continue. Always throw an Error object, not a string, because an Error carries 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(`can't withdraw ${amount} from ${balance}`);
}
return balance - amount;
}
try {
withdraw(50, 80);
} catch (error) {
console.log(error.name + ": " + error.message);
}RangeError: can't withdraw 80 from 50The built-in types (TypeError, RangeError, SyntaxError) already describe common problems. Use them where they fit.
When your app has its own failure types, make classes for them. Then callers can tell the difference between "the user typed bad data" and "the server exploded."
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validateAge(age) {
if (!Number.isInteger(age) || age < 0) {
throw new ValidationError("Age must be a positive whole number", "age");
}
return age;
}
try {
validateAge(-4);
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Fix the "${error.field}" field: ${error.message}`);
} else {
throw error;
}
}Fix the "age" field: Age must be a positive whole numberNotice the else { throw error; }. Only handle errors you understand. Re-throw the rest so they don't disappear.
A common mistake is wrapping everything in try/catch and logging the message. That hides problems. Ask yourself: at this level, can I actually recover?
function loadConfig(text) {
// No try/catch here. Callers decide what a bad config means.
return JSON.parse(text);
}
function startApp(text) {
try {
const config = loadConfig(text);
console.log("Started with", config);
} catch {
console.log("Config was invalid, using defaults");
}
}
startApp("not json");Config was invalid, using defaultsIf you don't need the error object, you can leave off the (error) part entirely, as shown above.
A try/catch only catches errors that happen while it's running. It works with await, because the code pauses inside the try:
async function fetchUser() {
throw new Error("network down");
}
async function main() {
try {
const user = await fetchUser();
console.log(user);
} catch (error) {
console.log("Caught:", error.message);
}
}
main();Caught: network downHere's where it goes wrong. Without await, the promise is created inside the try, but the failure happens later, after the try has already finished:
try {
fetchUser(); // missing await
} catch (error) {
console.log("never runs");
}For promise chains, add .catch() at the end. For code you don't await, attach a handler directly: fetchUser().catch(console.error).
fetch only rejects when the network fails. A 404 or 500 response counts as success as far as the promise is concerned.
async function getJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
}Always check response.ok. This one line prevents a lot of "why is my data undefined?" mysteries.
When you catch an error and throw a new one, keep the original attached using the cause option.
function readUser(text) {
try {
return JSON.parse(text);
} catch (error) {
throw new Error("Could not read user data", { cause: error });
}
}
try {
readUser("{bad");
} catch (error) {
console.log(error.message);
console.log("because:", error.cause.name);
}Could not read user data
because: SyntaxErrorThe high-level message tells you what went wrong in your app's terms. The cause keeps the low-level detail for debugging.
catch {} silently swallows problems. At least log something.throw "oops" has no stack trace. Use new Error("oops").try for risky code, catch to recover, finally for cleanup.Error objects, and make custom classes for your app's own failure types.await inside try, and check response.ok with fetch.Reading helps, but typing the code is what makes it stick. Open the editor, paste any example from this article, and change it until it breaks.
Open the JavaScript editorUnderstand JavaScript promises and async/await step by step, including running tasks in parallel with Promise.all and the mistakes that cause silent bugs.
Learn how to find elements, change text and classes, create new nodes, and handle clicks with event delegation, plus how to avoid common XSS mistakes.
A hands-on tour of the modern JavaScript features that make everyday code shorter and safer, from destructuring and ?. to structuredClone and toSorted.