Errors are inevitable in software. Networks drop packets, users submit invalid data, APIs return unexpected formats, and developers make mistakes. JavaScript, running in an event-driven single-threaded environment, presents unique challenges for error handling. An unhandled exception in a synchronous callback can crash a Node.js server. An unhandled Promise rejection in a browser can silently fail, leaving users staring at a frozen spinner. Even experienced developers search daily for javascript error handling patterns, try catch javascript syntax, and async await error handling best practices.
The problem is not a lack of documentation. MDN exhaustively documents every error type and every API. Stack Overflow contains millions of answers about javascript promise catch behavior and fetch error handling. The problem is retrieval friction — finding the right pattern at the right moment, understanding the subtle differences between Promise.all and Promise.allSettled, remembering that fetch does not throw on HTTP 404, or knowing when to use window.onerror versus unhandledrejection.
That is why we built the free interactive JavaScript Error Handling Patterns Cheat Sheet. It organizes 45+ error handling patterns into a searchable, filterable reference with copy-ready code examples. Whether you are debugging a production incident, writing defensive code for a new feature, or preparing for a technical interview, every pattern is at your fingertips. Everything runs client-side in your browser — no data ever leaves your machine.
Skip ahead: If you want to explore the tool while reading, open the JavaScript Error Handling Patterns Cheat Sheet in another tab. Use it to test the code examples in this article in real time.
Why JavaScript Error Handling Is Uniquely Challenging
JavaScript runs in two fundamentally different environments — browsers and Node.js — with different global error handling mechanisms. It supports both synchronous and asynchronous execution models, each with distinct error propagation paths. It has built-in error types that behave differently depending on context. And its most common networking API, fetch(), has counterintuitive error behavior that trips up even senior developers.
In a browser, an uncaught synchronous error triggers window.onerror. An unhandled Promise rejection triggers unhandledrejection. A resource loading error (broken image, failed script) triggers error on the element and bubbles to window. In Node.js, uncaught exceptions trigger process.on('uncaughtException') while unhandled rejections trigger process.on('unhandledRejection'). These handlers have different signatures, different best practices, and different consequences.
Asynchronous error handling adds another dimension. A Promise rejection without a .catch() handler becomes an unhandled rejection — but only after the microtask queue clears. An async function with an unawaited Promise inside a try/catch block silently bypasses the catch. Promise.all() rejects immediately on the first failure, abandoning all other pending promises. Promise.allSettled() waits for everything but returns a more complex data structure. Understanding these nuances is essential for writing robust JavaScript.
Try / Catch / Finally: The Foundation
The try...catch statement is the most fundamental error handling construct in JavaScript. It executes the catch block when any error is thrown in the try block. This works for synchronous code and, when combined with await, for asynchronous code too.
Basic Try / Catch
try {
const result = riskyOperation();
console.log(result);
} catch (error) {
console.error('Operation failed:', error.message);
} The catch binding receives an Error object (or any value that was thrown). By convention, this parameter is named error, err, or e. The error object contains message, name, and stack properties.
Try / Catch / Finally
The finally block always executes regardless of whether an error occurred. This makes it ideal for cleanup operations — closing files, releasing locks, clearing timers, or resetting UI state.
const file = openFile('data.txt');
try {
const data = file.read();
process(data);
} catch (error) {
console.error('Read failed:', error);
} finally {
file.close(); // Always runs, even if read() succeeds
} Importantly, if the finally block throws an error, that error replaces any previous error. If finally contains a return statement, it overrides any return or throw from the try/catch blocks.
Conditional Catch with instanceof
Not all errors should be handled the same way. Use instanceof to route different error types to different handlers.
try {
parseUserInput(input);
} catch (error) {
if (error instanceof TypeError) {
showToast('Invalid input type');
} else if (error instanceof RangeError) {
showToast('Value out of range');
} else {
logToSentry(error);
showToast('Unexpected error');
}
} This pattern requires custom error classes or the built-in error hierarchy to work effectively. We will cover custom errors later in this article.
Optional Catch Binding (ES2019)
Starting with ES2019, the catch binding is optional. Use this when you only care that an operation failed, not why.
try {
JSON.parse(maybeJson);
} catch {
// We only care that it failed, not why
useDefaultConfig();
} This produces slightly cleaner code and avoids unused variable warnings from linters.
Error Wrapping with Cause (ES2022)
ES2022 introduced the cause option for the Error constructor. This lets you wrap low-level errors with high-level context while preserving the original error for debugging.
try {
fetchUserData(userId);
} catch (error) {
throw new Error(
`Failed to load user ${userId}`,
{ cause: error }
);
}
// Later:
catch (error) {
console.log(error.message); // "Failed to load user 123"
console.log(error.cause); // original fetch error
} This is one of the most important patterns for building layered architectures. Each layer adds context without losing the root cause.
Promise Error Handling
Promises introduced a fundamentally different error model to JavaScript. Instead of throwing errors up the call stack, they carry rejections through a chain of .then() and .catch() handlers.
Promise.prototype.catch()
The .catch() method attaches a rejection handler to a promise. It returns a new promise, enabling chaining.
fetch('/api/user')
.then(res => res.json())
.catch(error => {
console.error('Fetch failed:', error);
return { error: true };
}); A critical detail: .catch() only handles rejections that occur before it in the chain. If the handler passed to .catch() itself throws, the new promise rejects with that error.
Promise.all vs Promise.allSettled
Promise.all() rejects immediately when any promise rejects. The other pending promises are abandoned — their results are lost.
Promise.all([
fetch('/api/a'),
fetch('/api/b'),
fetch('/api/c'),
])
.then(results => console.log(results))
.catch(error => {
// Only the FIRST rejection is caught here
console.error('At least one failed:', error);
}); Promise.allSettled(), introduced in ES2020, waits for all promises regardless of outcome. It returns an array of objects describing each result.
const results = await Promise.allSettled([
fetch('/api/a'),
fetch('/api/b'),
]);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.error('Failed:', result.reason);
}
}); Use Promise.all() when any failure means the entire operation should abort. Use Promise.allSettled() when you need to know the outcome of every promise and can handle partial failures.
Promise.race with Timeout
Promise.race() resolves or rejects as soon as any promise settles. This is the standard pattern for adding timeouts to async operations.
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([promise, timeout]);
}
withTimeout(fetch('/api/slow'), 5000)
.then(data => console.log(data))
.catch(err => console.error(err.message)); // "Timeout" Promise.any()
Promise.any() returns the first fulfilled promise. It only rejects if ALL promises reject, in which case it throws an AggregateError containing all rejection reasons.
try {
const fastest = await Promise.any([
fetchFromCDN1(),
fetchFromCDN2(),
fetchFromCDN3(),
]);
return fastest;
} catch (aggregateError) {
console.error('All CDNs failed:', aggregateError.errors);
} Async / Await Error Handling
Async/await makes asynchronous code look synchronous, but error handling requires attention to detail. The most common mistake is returning a promise without awaiting it inside a try/catch block.
The Classic Await Mistake
// BAD: try/catch does NOT catch this
async function bad() {
try {
return fetch('/api'); // Returns promise, does not await
} catch (error) {
// Never reached for fetch failures
}
}
// GOOD: await inside try
async function good() {
try {
return await fetch('/api'); // Awaits, so catch works
} catch (error) {
console.error('Caught:', error);
}
} This distinction is subtle but critical. When you return a promise without await, the try block succeeds immediately (it returns a promise object). The promise may later reject, but by then execution has left the try block.
Sequential Async with Error Isolation
When loading multiple independent resources, wrap each await in its own try/catch so one failure does not abort the entire sequence.
async function loadDashboard() {
let user = null;
let posts = [];
let notifications = [];
try {
user = await fetchUser();
} catch (e) {
console.error('User load failed');
}
try {
posts = await fetchPosts();
} catch (e) {
console.error('Posts load failed');
}
try {
notifications = await fetchNotifications();
} catch (e) {
console.error('Notifications load failed');
}
return { user, posts, notifications };
} Parallel Async with Individual Error Handling
For parallel execution, attach catch handlers to individual promises before passing them to Promise.all.
async function loadInParallel() {
const userPromise = fetchUser().catch(() => null);
const postsPromise = fetchPosts().catch(() => []);
const statsPromise = fetchStats().catch(() => ({}));
const [user, posts, stats] = await Promise.all([
userPromise, postsPromise, statsPromise
]);
return { user, posts, stats };
} This pattern runs all fetches concurrently but provides fallback values for any that fail. The dashboard renders even if some data is missing.
Custom Error Classes
Built-in error types cover generic cases, but real applications need domain-specific errors. Custom error classes enable precise error handling, better error messages, and structured logging.
Extending Error
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = 'DatabaseError';
this.query = query;
}
} Always set this.name to the class name. This ensures the stack trace and toString() output identify the correct error type. When extending Error in transpiled code, you may also need Object.setPrototypeOf(this, new.target.prototype) for instanceof to work correctly in some environments.
HTTP-Specific Errors
class HttpError extends Error {
constructor(response) {
super(`HTTP ${response.status}: ${response.statusText}`);
this.name = 'HttpError';
this.status = response.status;
}
}
class ClientError extends HttpError {}
class ServerError extends HttpError {}
async function apiFetch(url) {
const res = await fetch(url);
if (!res.ok) {
const ErrorClass = res.status >= 500 ? ServerError : ClientError;
throw new ErrorClass(res);
}
return res.json();
} Error Codes for Programmatic Handling
Add machine-readable error codes alongside human messages. This enables conditional logic without fragile string matching.
class AppError extends Error {
constructor(code, message, details = {}) {
super(message);
this.name = 'AppError';
this.code = code;
this.details = details;
}
}
// Usage:
throw new AppError(
'USER_NOT_FOUND',
'The requested user does not exist',
{ userId: 123 }
);
// Handler:
catch (error) {
if (error.code === 'USER_NOT_FOUND') {
redirectToOnboarding();
}
} Global Error Handlers
Global handlers are the safety net of last resort. They catch errors that bypass all local handlers. Every production application should have them.
window.onerror (Browser)
Captures uncaught synchronous exceptions in the main thread. Return true to prevent the default browser behavior (console error message).
window.onerror = (message, source, lineno, colno, error) => {
logToService({
type: 'uncaught_exception',
message,
source,
lineno,
colno,
stack: error?.stack,
});
return true; // Prevent default browser handling
}; unhandledrejection (Browser)
Fires when a Promise rejects without a .catch() handler. This is one of the most important events for async error monitoring.
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
logToService({
type: 'unhandled_rejection',
reason: event.reason?.message || event.reason,
stack: event.reason?.stack,
});
event.preventDefault();
}); Node.js process.on("uncaughtException")
In Node.js, uncaughtException is the last resort for synchronous errors. After this event fires, the application is in an undefined state. You must exit.
process.on('uncaughtException', (error, origin) => {
console.error(`Uncaught Exception at ${origin}`);
console.error(error);
// Graceful shutdown
server.close(() => {
process.exit(1);
});
// Force exit if graceful shutdown hangs
setTimeout(() => process.exit(1), 5000);
}); Fetch and Network Error Handling
The fetch API has counterintuitive error behavior that confuses many developers. It does not throw on HTTP error status codes.
Always Check response.ok
const response = await fetch('/api/data');
if (!response.ok) {
// response.ok is false for 4xx and 5xx
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json(); fetch() only rejects on network-level failures: DNS errors, CORS violations, connection resets, and request timeouts (if configured via AbortController). A 404 or 500 response resolves successfully.
AbortController for Timeouts
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('/api/slow', {
signal: controller.signal,
});
clearTimeout(timeoutId);
return response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.error('Request timed out');
} else {
console.error('Network error:', error);
}
} Retry with Exponential Backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i <= maxRetries; i++) {
try {
const res = await fetch(url, options);
if (res.ok) return res;
if (res.status < 500) throw new Error(`Client error ${res.status}`);
} catch (error) {
if (i === maxRetries) throw error;
}
const delay = Math.pow(2, i) * 1000;
await new Promise(r => setTimeout(r, delay));
}
} Built-in JavaScript Error Types
JavaScript provides several built-in error constructors. Understanding them helps with debugging and enables precise error handling.
TypeError
Thrown when a value is not of the expected type, or when accessing properties of null or undefined. This is the most common runtime error in JavaScript applications.
null.toString(); // TypeError: Cannot read properties of null
undefined.foo; // TypeError: Cannot read properties of undefined
(42).toUpperCase(); // TypeError: toUpperCase is not a function ReferenceError
Thrown when referencing a variable that does not exist in the current scope. Also thrown for temporal dead zone violations with let and const.
console.log(undefinedVar); // ReferenceError: undefinedVar is not defined
let x = y; // ReferenceError: Cannot access 'y' before initialization RangeError
Thrown when a numeric value is outside the allowed range, or when the call stack exceeds its maximum size.
new Array(-1); // RangeError: Invalid array length
(1).toFixed(101); // RangeError: toFixed() digits argument out of range
function recurse() { recurse(); }
recurse(); // RangeError: Maximum call stack size exceeded SyntaxError
Thrown when the JavaScript engine encounters invalid syntax. Note that SyntaxErrors in loaded scripts cannot be caught at runtime in the same file. However, SyntaxErrors from eval(), Function(), or JSON.parse() can be caught.
JSON.parse('{ invalid }'); // SyntaxError: Unexpected token
try {
JSON.parse('{ invalid }');
} catch (e) {
if (e instanceof SyntaxError) {
console.log('Invalid JSON:', e.message);
}
} AggregateError
Represents multiple errors in a single exception. Used by Promise.any() when all promises reject, and available for custom use.
try {
await Promise.any([reject1, reject2, reject3]);
} catch (error) {
console.log(error instanceof AggregateError); // true
console.log(error.errors.length); // 3
error.errors.forEach(e => console.log(e.message));
} Error Handling Best Practices
Beyond syntax and API knowledge, effective error handling requires discipline and architecture. Here are the patterns that separate robust applications from fragile ones.
Fail Fast with Explicit Throws
Validate inputs at function boundaries and throw immediately. Do not let invalid data propagate through your system.
function calculateDiscount(price, discountPercent) {
if (typeof price !== 'number' || price < 0) {
throw new TypeError('price must be a non-negative number');
}
if (typeof discountPercent !== 'number' ||
discountPercent < 0 || discountPercent > 100) {
throw new RangeError('discountPercent must be 0-100');
}
return price * (discountPercent / 100);
} Never Swallow Errors Silently
Empty catch blocks are one of the most destructive patterns in JavaScript. They hide bugs, obscure failures, and make debugging impossible.
// BAD: Error disappears
} catch (e) { }
// BETTER: Log and re-throw
} catch (error) {
logger.error('Database query failed', error);
throw error;
}
// BEST: Handle meaningfully
} catch (error) {
if (isRetryable(error)) {
return retry(operation);
}
logger.error('Fatal:', error);
throw error;
} Distinguish Operational vs Programmer Errors
Operational errors are expected runtime failures: network timeouts, disk full, invalid user input. Programmer errors are bugs: null references, type mismatches, invariant violations. Operational errors should be handled gracefully. Programmer errors should crash fast to expose the bug.
Centralized Error Logging
Route all errors through a single logger to ensure consistent formatting, sampling, and transport to monitoring services.
class ErrorLogger {
static log(error, context = {}) {
const payload = {
message: error.message,
stack: error.stack,
name: error.name,
timestamp: new Date().toISOString(),
...context,
};
if (process.env.NODE_ENV === 'production') {
sendToSentry(payload);
} else {
console.error('[ERROR]', payload);
}
}
} React Error Boundaries
In React applications, Error Boundaries catch rendering errors in child components without crashing the entire application tree.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
logErrorToService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
return <FallbackUI />;
}
return this.props.children;
}
} Defensive Default Values
When recovering from errors, provide sensible defaults instead of null or undefined to prevent cascading failures.
let config;
try {
config = JSON.parse(localStorage.getItem('config'));
} catch {
config = getDefaultConfig();
}
// Ensure config is always a valid object
config = config || getDefaultConfig();
function getDefaultConfig() {
return { theme: 'light', language: 'en', notifications: true };
} Related Tools and References
Error handling is just one part of writing robust JavaScript. Explore these related tools in the DevToolkit collection:
- JavaScript Array Methods Cheat Sheet — 40+ array methods with mutating vs non-mutating labels
- JavaScript String Methods Cheat Sheet — Complete string manipulation reference
- JavaScript Date/Time Methods Cheat Sheet — Date parsing, formatting, and manipulation
- React Hooks Cheat Sheet — useEffect, useState, and custom hook patterns
- React Performance Patterns Cheat Sheet — Memoization, lazy loading, and optimization
- JSON Formatter — Validate and format JSON responses from APIs
- JavaScript Minifier — Compress JavaScript for production
- Node.js Built-in Modules Cheat Sheet — 70+ Node.js APIs across fs, path, http, events, stream, buffer, crypto, os, and more
- JavaScript ES2024+ Features Cheat Sheet — Modern JavaScript APIs including Promise.withResolvers, Object.groupBy, iterator helpers, and set operations
Try the Interactive Cheat Sheet
Reading about error handling is valuable, but having a searchable reference at your fingertips is transformative. Our free interactive JavaScript Error Handling Patterns Cheat Sheet includes all 45+ patterns from this article, organized into filterable categories, with syntax highlighting and one-click copy. Whether you need a quick reminder of Promise.allSettled behavior, a custom error class template, or the exact signature of window.onerror, it is all there.
The cheat sheet runs entirely in your browser. No account required. No data collection. Just open it and start searching.