JavaScript Error Handling
In production, things will go wrong: APIs fail, users enter garbage data, networks drop. Error handling is the difference between your app showing a helpful message and showing a blank white screen.
An error occurs when JavaScript encounters a problem that prevents the current operation from completing normally. JavaScript can throw an error, and if it is not handled, the current execution flow is interrupted and the error becomes uncaught.
Errors are part of real development. A user can enter invalid input, an API can fail, or data can come in a shape you did not expect. Good error handling helps your app fail gracefully instead of breaking completely.
Built-in JavaScript error objects are instances of the Error class or one of its subclasses. An error object has two key properties:
- name - the type of error (e.g. "TypeError", "ReferenceError")
- message - a human-readable description of what went wrong
- stack - a stack trace showing where the error originated (very useful for debugging)
try {
null.toString(); // null has no methods - this throws a TypeError
} catch (e) {
console.log(e.name); // TypeError
console.log(e.message); // Cannot read properties of null (reading 'toString')
console.log(e instanceof TypeError); // true
console.log(e instanceof Error); // true - all error types extend Error
}
A synchronous error thrown while a try block is executing can be caught by its catch. A rejected Promise is not automatically caught by a surrounding try...catch; handle it with .catch() or use await inside the try block.
function loadSettings() {
return Promise.reject(new Error("Settings unavailable"));
}
// Handle a rejected Promise with .catch()
loadSettings()
.then(settings => console.log(settings))
.catch(error => {
console.error("Could not load settings:", error.message);
});
// The equivalent async/await form
async function showSettings() {
try {
const settings = await loadSettings();
console.log(settings);
} catch (error) {
console.error("Could not load settings:", error.message);
}
}
showSettings();
A try...catch only catches errors thrown during execution of its try block. A Promise rejection must be handled with await inside that block or with .catch() on the Promise.
The fetch() API adds another detail: network failures reject the Promise, but a response such as 404 or 500 normally resolves. Check response.ok or response.status; throwing an error when the status is unsuccessful deliberately converts the HTTP failure into a rejected Promise that catch can handle.
// HTML: <p id="status"></p>
async function loadUser() {
try {
const response = await fetch("/api/user");
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("User request failed:", error.message);
throw error; // let the UI decide how to display the failure
}
}
loadUser().catch(() => {
document.querySelector("#status").textContent = "Could not load your profile.";
});
The try...catch statement is the main way to handle runtime errors in JavaScript. The try block contains code that may throw. If an error is thrown while it is executing, control moves to catch, which receives the thrown value. JavaScript conventionally throws Error objects or subclasses. If the error is handled, execution continues after the try...catch statement.
try {
// Code that might fail
const data = JSON.parse("this is not valid json");
} catch (e) {
// Code that runs only when an error occurs
console.log("Something went wrong:", e.message);
}
// Execution continues here regardless
console.log("Done");
The catch block receives the error object. You can name it anything - e, err, error are all common. Use it to log, display, or recover from the problem:
function parseUserInput(input) {
try {
const result = JSON.parse(input);
return result;
} catch (err) {
console.error("Invalid JSON input:", err.message);
return null; // return a safe default instead of crashing
}
}
console.log(parseUserInput('{"product": "Laptop", "price": 999}')); // { product: 'Laptop', price: 999 }
console.log(parseUserInput("bad data")); // null - error handled
A common mistake is assuming that try...catch automatically handles every fetch() failure. fetch() rejects for network failures, but HTTP responses such as 404 or 500 normally resolve successfully. Check response.ok or response.status when HTTP status errors should be treated as failures.
The finally block runs after the try/catch processing, whether the operation succeeds or an error is caught. It also runs when control leaves the flow with a return.
Use it for cleanup work that must always happen - closing a database connection, hiding a loading spinner, releasing a lock:
function fetchData(url) {
console.log("Loading...");
try {
// Simulate a fetch that might fail
if (!url) throw new Error("URL is required");
console.log("Fetched:", url);
} catch (e) {
console.error("Fetch failed:", e.message);
} finally {
console.log("Loading complete."); // always runs
}
}
fetchData("https://api.example.com"); // Fetched, then Loading complete
fetchData(""); // Fetch failed, then Loading complete
finally is optional. You can have try...catch, try...finally, or try...catch...finally. The catch is also optional, but you must have either catch or finally.
Watch out: If finally itself throws an error or has a return statement, it overrides any value returned from try or catch. Keep finally focused on cleanup only.The throw statement stops the current execution flow and passes the thrown value to the nearest matching catch. If no local handler matches, the error propagates to the caller.
JavaScript technically allows any value to be thrown, including a string, number, or object. The recommended convention is to throw an Error instance or subclass, because it provides useful name, message, and stack information:
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
try {
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // throws
} catch (e) {
console.error(e.message); // Cannot divide by zero
}
Use throw inside validation functions to signal that input is bad, rather than returning null or false and hoping the caller checks it:
function createUser(name, age) {
if (typeof name !== "string" || name.trim() === "") {
throw new TypeError("name must be a non-empty string");
}
if (typeof age !== "number" || age < 0 || age > 150) {
throw new RangeError("age must be a number between 0 and 150");
}
return { name, age };
}
try {
createUser("", 25); // throws TypeError
} catch (e) {
console.log(e.name); // TypeError
console.log(e.message); // name must be a non-empty string
}
JavaScript has several built-in error types. Each one extends the base Error class and signals a different kind of problem. In everyday code, TypeError, ReferenceError, SyntaxError, and RangeError are especially useful to recognize:
- Error - the generic base. Use it when no more specific type fits.
- SyntaxError - invalid JavaScript syntax (e.g., broken JSON in JSON.parse()).
- ReferenceError - accessing a variable that does not exist.
- TypeError - wrong type used (e.g., calling a non-function, accessing property on null).
- RangeError - a numeric value is outside the allowed range (e.g., invalid array length).
- URIError - malformed URI in decodeURIComponent().
- EvalError - related to eval(). Rarely seen in practice.
// ReferenceError - variable not declared
try {
console.log(undeclaredVar);
} catch (e) {
console.log(e.name); // ReferenceError
}
// TypeError - wrong type
try {
null.toString();
} catch (e) {
console.log(e.name); // TypeError
}
// RangeError - out of range
try {
new Array(-1);
} catch (e) {
console.log(e.name); // RangeError
}
// SyntaxError - invalid JSON
try {
JSON.parse("{bad json}");
} catch (e) {
console.log(e.name); // SyntaxError
}
In a catch block, you can check the error type to decide how to handle it:
try {
riskyOperation();
} catch (e) {
if (e instanceof TypeError) {
console.log("Type problem:", e.message);
} else if (e instanceof RangeError) {
console.log("Range problem:", e.message);
} else {
throw e; // re-throw errors you don't know how to handle
}
}
Advanced: Custom error classes are especially useful in larger applications. Extend Error to define domain-specific errors with meaningful names, making your catch blocks cleaner and your intent clearer.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = "DatabaseError";
this.query = query; // add custom properties
}
}
// Usage
try {
throw new ValidationError("Email is required");
} catch (e) {
console.log(e.name); // ValidationError
console.log(e.message); // Email is required
console.log(e instanceof ValidationError); // true
console.log(e instanceof Error); // true
}
Custom errors are especially useful in larger applications. Instead of checking e.message with string comparisons, you check e instanceof YourErrorClass - which is much more reliable:
function processForm(data) {
if (!data.price) throw new ValidationError("Price is required");
if (!data.product) throw new ValidationError("Product name is required");
// ... save to DB
}
try {
processForm({ product: "Laptop" }); // missing price
} catch (e) {
if (e instanceof ValidationError) {
console.log("Form error:", e.message); // Form error: Price is required
} else {
throw e; // unexpected - re-throw
}
}
If an error is thrown and not caught in the current function, it propagates to the caller. This continues up the call stack until a matching catch handles it or the error becomes uncaught.
throws error level2()
does not catch level1()
catch handles it caller continues
after catch
If no caller catches the error, it continues to the top level and becomes an uncaught error.
function level3() {
throw new Error("Something broke in level 3");
}
function level2() {
level3(); // no try/catch here - error bubbles up
}
function level1() {
try {
level2();
} catch (e) {
console.log("Caught in level1:", e.message);
// Caught in level1: Something broke in level 3
}
}
level1();
A common pattern is to catch an error, do something (like log it), and then re-throw it so the caller still knows something went wrong. This is useful when you can partially handle an error but not fully:
function loadConfig(path) {
try {
// attempt to read config
return JSON.parse(readFile(path));
} catch (e) {
if (e instanceof SyntaxError) {
console.error("Config file has invalid JSON:", e.message);
throw e; // re-throw - the caller needs to know this failed
}
// For other errors (file not found etc.), silently return defaults
return {};
}
}
- An error is an object with name, message, and stack properties. All error types extend the base Error class.
- try...catch: code in try runs normally. If it throws, control jumps to catch. Execution continues after the block either way.
- finally: always runs regardless of success or failure. Use it for cleanup code that must execute no matter what.
- throw: create and throw your own errors. JavaScript allows any thrown value, but the recommended convention is an Error instance (or subclass), not a raw string.
- Error types: TypeError, ReferenceError, RangeError, SyntaxError, and others. Use instanceof in catch to handle specific types differently.
- Custom errors: extend Error to create domain-specific errors. Set this.name in the constructor to give it a meaningful name.
- Error propagation: uncaught errors bubble up the call stack. Catch only what you can handle. Re-throw errors you cannot fully handle.
- Async errors: handle rejected Promises with .catch() or await inside try...catch. Check response.ok because fetch() does not reject for HTTP 4xx or 5xx responses.
- What is the difference between try...catch and try...catch...finally?
- When does the finally block run?
- What types of errors does try...catch not catch?
- What is the difference between TypeError and ReferenceError?
- What does throw do and what should you throw?
- How do you create a custom error class in JavaScript?
- What is error propagation and how does it work?
- When should you re-throw an error instead of handling it?
- How do you handle errors in asynchronous code?
- What is the difference between e.message and e.stack?