Error Handling in JavaScript
This Article will help you to write good code.

Every program makes assumptions. It assumes the network is available. It assumes the user gave you a valid input. It assumes the file exists. It assumes the API response has the shape you expect.
Most of the time those assumptions hold. Then they don't and what happens next is entirely up to you.
JavaScript gives you a system for managing failures without letting them crash the whole program. Understanding it isn't just about catching errors. It's about writing code that fails gracefully code that tells you exactly what went wrong, cleans up after itself, and gives the user something useful instead of a blank screen or a frozen UI.
What errors actually are in JavaScript
When JavaScript encounters something it can't handle dividing by something illegal, calling a method on undefined, trying to parse malformed JSON it creates an Error object and throws it. This throw unwinds the call stack, skipping everything below it, until something catches it or it reaches the top level and crashes your program.
const user = null;
console.log(user.name); // TypeError: Cannot read properties of null
That TypeError is a real object. It has a message (what went wrong), a name (the type of error), and a stack (the trace of function calls that led here). JavaScript creates it, throws it, and your program stops unless something is listening for it.
JavaScript has several built-in error types:
TypeError(wrong type)ReferenceError(variable doesn't exist)SyntaxError(invalid code)RangeError(value out of bounds).
They all inherit from the base Error class.
The try/catch block
try/catch lets you put risky code in a guarded zone. If anything throws inside try, execution jumps immediately to catch and you decide what to do with the error instead of letting it crash your program.
try {
const data = JSON.parse("this is not valid json");
console.log(data);
} catch (error) {
console.error("Failed to parse:", error.message);
}
// Failed to parse: Unexpected token 'h', "this is n"... is not valid JSON
Without try/catch, that JSON.parse throws a SyntaxError and everything stops. With it, you catch the error, log it clearly, and your program keeps running.
The error object passed to catch is whatever was thrown. It has two properties you'll use constantly:
try {
undeclaredVariable;
} catch (error) {
console.log(error.name); // "ReferenceError"
console.log(error.message); // "undeclaredVariable is not defined"
}
The finally block
finally is a block that runs no matter what whether the try succeeded, whether the catch triggered, whether an error was re-thrown. It always runs.
function loadData() {
showSpinner();
try {
const result = fetchSomething();
display(result);
} catch (error) {
showErrorMessage(error.message);
} finally {
hideSpinner(); // always runs — success or failure
}
}
hideSpinner() runs whether fetchSomething succeeded or crashed. Without finally, you'd have to call hideSpinner() in both the try and the catch, duplicating cleanup logic. finally exists precisely for this: teardown, cleanup, resource release anything that must happen regardless of outcome.
Here's the full execution order laid out clearly:
Throwing custom errors
JavaScript lets you throw anything but throwing a proper Error object (or a subclass of it) is best practice because it gives you a stack trace and a message.
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
try {
const result = divide(10, 0);
} catch (error) {
console.error(error.message); // "Cannot divide by zero"
}
You're not waiting for JavaScript to fail you're proactively saying "this situation is invalid" and throwing your own error. The caller's catch block handles it just like any built-in error.
For larger applications, creating custom error classes makes your error handling more precise you can catch specific error types and respond differently:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = "NetworkError";
this.statusCode = statusCode;
}
}
function processForm(data) {
if (!data.email.includes("@")) {
throw new ValidationError("Invalid email address", "email");
}
}
try {
processForm({ email: "notanemail" });
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Validation failed on field: ${error.field}`);
highlightField(error.field);
} else if (error instanceof NetworkError) {
console.log(`Network failed with status: ${error.statusCode}`);
showRetryButton();
} else {
console.error("Unexpected error:", error);
}
}
instanceof lets you branch on error type different failures get different responses instead of one generic "something went wrong" message.
Error handling with async/await
try/catch works exactly the same way with async/await. Rejected promises become catchable errors:
async function loadUserData(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new NetworkError("Request failed", response.status);
}
const user = await response.json();
return user;
} catch (error) {
if (error instanceof NetworkError) {
console.error(`HTTP \({error.statusCode}: \){error.message}`);
} else {
console.error("Unexpected error:", error.message);
}
return null;
} finally {
hideLoadingIndicator();
}
}
Same underlying problem. Completely different user experience. Error handling is how you translate a technical failure into a useful human message.
A pattern worth building into any serious application:
async function safeLoadDashboard() {
try {
const [user, stats, notifications] = await Promise.all([
fetchUser(),
fetchStats(),
fetchNotifications()
]);
renderDashboard(user, stats, notifications);
} catch (error) {
console.error("Dashboard load failed:", error);
renderErrorState("Unable to load dashboard. Please refresh.");
} finally {
hideGlobalSpinner();
}
}
If any of the three fetches fail, catch handles it.





