
Error handling is one of those fundamental pillars when writing JavaScript that separates sloppy, fragile code from resilient, maintainable apps. At its core, handling errors means anticipating that things will go off the rails and having a formalized plan to deal with the fallout gracefully.
The most common construct for capturing errors is the try...catch block. You wrap risky code inside try, and if it throws, control immediately transfers to catch, where you can examine and respond to the error.
try {
// Code that might throw
let data = JSON.parse(someString);
console.log(data.name);
} catch (err) {
// Recover or report
console.error("Failed to parse JSON:", err);
}
Simple enough, yet often overlooked is that not every error scenario needs a catch block. If try surrounds an operation you can handle, like recovering from bad input, catch is your tool. But if your entire function can’t handle an error meaningfully, sometimes it’s better to let it bubble to a higher context with a caller that knows what to do.
Another baseline: you can throw anything in JavaScript, although throwing objects that subclass Error (or at least are Error instances) is strongly recommended for consistency. This preserves meaningful stack traces and lends semantic clarity.
throw new TypeError("Expected a string but got number");
// instead of
throw "Oops something went wrong";
Without error classes, it’s difficult to differentiate what error actually occurred versus a plain string, especially when errors cross asynchronous boundaries or multiple system layers. This comes back to the “first principles” of error handling—errors are data.
When an error is thrown, the JavaScript runtime starts unwinding the call stack looking for the nearest matching catch. If none is found, the program either terminates or, in browsers, reports the error globally. This stack-unwinding behavior is why encapsulating your codebase’s risky spots in try-catch blocks makes your program robust—errors are contained before wreaking havoc.
Even when asynchronous code is involved, proper error handling can be achieved with async/await combined with try-catch. It is no excuse to ignore errors just because you’re inside a promise chain.
async function fetchUser(id) {
try {
let response = await fetch(/users/${id});
let user = await response.json();
return user;
} catch (err) {
console.error("Failed to fetch user:", err);
}
}
Ignoring catch blocks or swallowing errors with empty catch clauses causes subtle bugs that are difficult to debug. There’s a reason linting tools warn against empty catches—it’s effectively throwing away valuable error data.
Lastly, a good grasp of error handling recognizes when you want to catch and recover, and when you simply want to catch to log or augment before letting the error pass up. The difference is nuanced but critical for building fault-tolerant systems that still fail noisily and informatively when they must.
Garmin Forerunner 165, Running Smartwatch, Colorful AMOLED Display, Training Metrics and Recovery Insights, Black
Now retrieving the price.
(as of June 3, 2026 23:09 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Preserving the original error stack when rethrowing
When rethrowing an error, it is vital to maintain the original stack trace to facilitate debugging. By default, when you throw an error again, the stack trace points to the line where you rethrew the error, obscuring the original context where it was first thrown. To preserve the original stack trace, you can use the Error constructor.
One common pattern is to catch the error, then create a new error while attaching the original error as a property. This way, you can log both the new error and the original one, giving you a complete picture of what went wrong.
try {
// Some operation that may fail
await riskyOperation();
} catch (originalError) {
const newError = new Error("Failed to complete risky operation");
newError.originalError = originalError;
throw newError;
}
This method provides clarity in your logs, so that you can trace back to the original error while still conveying the context of the failure. It’s particularly useful in larger systems where multiple layers of abstraction can obscure the source of an error.
Another approach is to use the Object.setPrototypeOf method to maintain the prototype chain of the original error. This way, you can throw a new error this is essentially a derivative of the original one, preserving its stack trace.
try {
// Code that might throw
throw new Error("An error occurred");
} catch (err) {
const newError = new Error("Something went wrong");
Object.setPrototypeOf(newError, err);
throw newError;
}
While this approach is more complex, it can be beneficial in scenarios where you want to create custom error types while retaining the original error’s context. However, it’s essential to be cautious with this technique as it can lead to confusion if not documented well.
In practice, the best way to preserve the stack trace is to use the Error.captureStackTrace method, which is available in V8-based environments like Node.js and modern browsers. This method allows you to create a new error and specify where the stack trace should start.
function customError(message) {
const error = new Error(message);
Error.captureStackTrace(error, customError);
return error;
}
try {
throw customError("Custom error occurred");
} catch (err) {
console.error(err.stack);
}
This method ensures that the stack trace is captured accurately at the point of error creation, not where the error is thrown. It’s a powerful tool for building robust error handling in your applications.
When implementing error handling, consider the implications of how you rethrow errors. Each method of rethrowing can affect how easily you can trace issues in your application, so it’s worth investing time to choose the right pattern. Understanding the nuances of error propagation can significantly enhance your debugging capabilities.
Best practices for rethrowing errors in production code
When rethrowing an error, it is vital to maintain the original stack trace to facilitate debugging. By default, when you throw an error again, the stack trace points to the line where you rethrew the error, obscuring the original context where it was first thrown. To preserve the original stack trace, you can use the Error constructor.
One common pattern is to catch the error, then create a new error while attaching the original error as a property. This way, you can log both the new error and the original one, giving you a complete picture of what went wrong.
try {
// Some operation that may fail
await riskyOperation();
} catch (originalError) {
const newError = new Error("Failed to complete risky operation");
newError.originalError = originalError;
throw newError;
}
This method provides clarity in your logs, enabling you to trace back to the original error while still conveying the context of the failure. It’s particularly useful in larger systems where multiple layers of abstraction can obscure the source of an error.
Another approach is to use the Object.setPrototypeOf method to maintain the prototype chain of the original error. This way, you can throw a new error that is essentially a derivative of the original one, preserving its stack trace.
try {
// Code that might throw
throw new Error("An error occurred");
} catch (err) {
const newError = new Error("Something went wrong");
Object.setPrototypeOf(newError, err);
throw newError;
}
While this approach is more complex, it can be beneficial in scenarios where you want to create custom error types while retaining the original error’s context. However, it’s essential to be cautious with this technique as it can lead to confusion if not documented well.
In practice, the best way to preserve the stack trace is to use the Error.captureStackTrace method, which is available in V8-based environments like Node.js and modern browsers. This method allows you to create a new error and specify where the stack trace should start.
function customError(message) {
const error = new Error(message);
Error.captureStackTrace(error, customError);
return error;
}
try {
throw customError("Custom error occurred");
} catch (err) {
console.error(err.stack);
}
This method ensures that the stack trace is captured accurately at the point of error creation, not where the error is thrown. It’s a powerful tool for building robust error handling in your applications.
When implementing error handling, consider the implications of how you rethrow errors. Each method of rethrowing can affect how easily you can trace issues in your application, so it’s worth investing time to choose the right pattern. Understanding the nuances of error propagation can significantly enhance your debugging capabilities.
