JavaScript Error Handling Cheat Sheet
Covers try/catch/finally, custom Error subclasses, handling errors in promises and async/await, and the ES2022 error cause chain.
try/catch/finally Basics
Core syntax for catching and cleaning up after errors.
try { const data = JSON.parse(input); // may throw SyntaxError process(data);} catch (err) { console.error('Failed:', err.message); // handle the error} finally { cleanup(); // always runs, even after return/throw}// Optional catch binding (ES2019) - omit the error variabletry { riskyOperation();} catch { console.log('Something went wrong');}
Custom Error Classes
Extend Error to create typed, catchable error subclasses.
class ValidationError extends Error { constructor(message, field) { super(message); this.name = 'ValidationError'; // shows up in stack traces this.field = field; }}function validate(age) { if (age < 0) throw new ValidationError('Age cannot be negative', 'age');}try { validate(-1);} catch (err) { if (err instanceof ValidationError) { console.log(`${err.field}: ${err.message}`); }}
Errors in Promises & async/await
Handle rejections in async functions, promise chains, and globally.
// async/await - wrap in try/catchasync function loadUser(id) { try { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { console.error('loadUser failed:', err); throw err; // re-throw so callers can react too }}// Promise chains - use .catch()fetchData() .then(process) .catch(err => console.error('Pipeline failed:', err));// Unhandled rejectionswindow.addEventListener('unhandledrejection', e => { console.error('Unhandled:', e.reason);});
Built-in Error Types & Properties
Standard error constructors and the fields available on any error.
- Error- Base error type; constructor accepts (message, options) where options.cause sets a chained cause
- TypeError- Thrown when a value is not of the expected type, e.g. calling a non-function
- RangeError- Thrown when a numeric value is outside its allowed range, e.g. invalid array length
- SyntaxError- Thrown when parsing invalid JS or JSON.parse fails
- ReferenceError- Thrown when referencing an undeclared variable
- err.message- Human-readable description of the error
- err.stack- Non-standard but widely supported stack trace string
- err.cause- ES2022 option for chaining the underlying cause: new Error('msg', { cause: original })
Error Cause Chaining (ES2022)
Preserve the original error when wrapping it in a higher-level one.
try { await connectToDb();} catch (dbErr) { throw new Error('Failed to start server', { cause: dbErr }); // preserves original error}// Later, when logging:catch (err) { console.error(err.message); console.error('Caused by:', err.cause);}
AggregateError for Multiple Failures
Represent and unpack several simultaneous errors as a single throwable value.
// Thrown automatically when every promise in Promise.any rejectstry { await Promise.any([Promise.reject('a'), Promise.reject('b')]);} catch (err) { console.log(err instanceof AggregateError); // true console.log(err.errors); // ['a', 'b']}// You can also construct one manually to report batch failuresfunction validateAll(items) { const errors = items .map(item => { try { validate(item); return null; } catch (e) { return e; } }) .filter(Boolean); if (errors.length) throw new AggregateError(errors, 'Batch validation failed');}
Global Error & Rejection Handlers
Catch errors that escape every local try/catch, in the browser and in Node.
// Browser: synchronous errors that were never caughtwindow.addEventListener('error', (event) => { console.error('Uncaught:', event.message, event.filename, event.lineno); event.preventDefault(); // stop the default browser console logging});// Browser: rejected promises with no .catch()window.addEventListener('unhandledrejection', (event) => { reportToSentry(event.reason); event.preventDefault();});// Node.js equivalentsprocess.on('uncaughtException', (err) => { logger.fatal(err); process.exit(1); // process is in an undefined state - do not resume});process.on('unhandledRejection', (reason) => { logger.error('Unhandled rejection:', reason);});
Retry with Exponential Backoff
A reusable pattern for retrying transient failures without hammering a failing service.
async function retry(fn, { attempts = 4, baseDelay = 200 } = {}) { let lastErr; for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (err) { lastErr = err; if (i === attempts - 1) break; // last attempt - don't wait, just fail const delay = baseDelay * 2 ** i + Math.random() * 100; // jitter avoids thundering herd await new Promise(r => setTimeout(r, delay)); } } throw new Error(`Failed after ${attempts} attempts`, { cause: lastErr });}await retry(() => fetch('/flaky-endpoint').then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json();}));
Structured Error Hierarchy with toJSON
Give custom errors machine-readable codes and safe serialization for logging/API responses.
class AppError extends Error { constructor(message, { code, statusCode = 500, cause } = {}) { super(message, { cause }); this.name = this.constructor.name; this.code = code; this.statusCode = statusCode; Error.captureStackTrace?.(this, this.constructor); // V8 only - trims constructor frame } toJSON() { // JSON.stringify(err) is {} by default without this return { name: this.name, message: this.message, code: this.code, statusCode: this.statusCode }; }}class NotFoundError extends AppError { constructor(resource) { super(`${resource} not found`, { code: 'NOT_FOUND', statusCode: 404 }); }}JSON.stringify(new NotFoundError('User')); // {"name":"NotFoundError","message":"User not found",...}
Subtle Gotchas & Advanced Behavior
Behaviors that surprise even experienced developers.
- finally overrides return/throw- A return or throw inside finally silently discards the try/catch block's own return value or exception
- catch block scoping- The bound error variable is block-scoped to the catch clause only, not the whole function
- Async errors need await- Forgetting to await/return an async call inside try means the rejection escapes the catch entirely
- instanceof across realms- An Error thrown in one iframe/vm context fails instanceof Error checks in another - compare err.name instead
- console.error vs throw- console.error() only logs; it doesn't stop execution or trigger catch/unhandledrejection
- Error subclassing pre-ES2015 targets- Transpiling to old targets can break instanceof for Error subclasses unless Object.setPrototypeOf is applied in the constructor
- Rethrowing loses async stack- Re-throwing across await boundaries can produce a stack trace that no longer shows the original call site clearly
Always throw Error objects (or subclasses), never plain strings or objects - only Error instances capture a stack trace, and tools like Sentry rely on err.stack to be useful.