Error Handling in JavaScript: The Complete Guide
Stop your app from crashing. Handle errors like a pro.
The Basics
// try/catch/finally
try {
const data = JSON.parse(userInput);
console.log(data.name);
} catch (error) {
console.error('Failed to parse:', error.message);
} finally {
// Always runs — cleanup code here
console.log('Parse attempt finished');
}
// Throwing errors
throw new Error('Something went wrong');
throw new TypeError('Expected a string');
throw new RangeError('Index out of bounds');
throw new ValidationError('Invalid email'); // Custom!
Custom Error Classes
class AppError extends Error {
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.code = code;
this.timestamp = new Date().toISOString();
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}
class ValidationError extends AppError {
constructor(fields) {
super('Validation failed', 422, 'VALIDATION_ERROR');
this.fields = fields; // { field: ['error1', 'error2'] }
}
}
class RateLimitError extends AppError {
constructor(retryAfter = 60) {
super('Too many requests', 429, 'RATE_LIMITED');
this.retryAfter = retryAfter;
}
}
// Usage
try {
if (!user) throw new NotFoundError('User');
if (!isValidEmail(email)) throw new ValidationError({ email: ['Invalid format'] });
} catch (error) {
if (error instanceof NotFoundError) {
res.status(404).json({ error: error.message });
} else if (error instanceof ValidationError) {
res.status(422).json({ error: error.message, details: error.fields });
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
Async Error Handling
Pattern 1: try/catch with async/await
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
if (response.status === 404) throw new NotFoundError('User');
if (response.status === 429) throw new RateLimitError(60);
throw new AppError(`HTTP ${response.status}`, response.status);
}
return await response.json();
} catch (error) {
if (error instanceof AppError) throw error;
throw new AppError(`Network error: ${error.message}`, 503, 'NETWORK_ERROR');
}
}
Pattern 2: Higher-Order Function Wrapper
// Wrap any async function to always return [data, error]
function asyncHandler(fn) {
return (...args) => fn(...args).catch(error => [undefined, error]);
}
// Usage — no try/catch needed!
const [user, error] = await asyncHandler(fetchUser)(123);
if (error) return handleError(error);
console.log(user.name);
Pattern 3: Result Type Pattern
class Result {
static ok(value) { return new Result(null, value); }
static err(error) { return new Result(error, null); }
#error;
#value;
constructor(error, value) {
this.#error = error;
this.#value = value;
}
get isOk() { return this.#error === null; }
get isErr() { return this.#error !== null; }
get value() { return this.#value; }
get error() { return this.#error; }
map(fn) {
return this.isOk ? Result.ok(fn(this.#value)) : this;
}
flatMap(fn) {
return this.isOk ? fn(this.#value) : this;
}
orElse(defaultValue) {
return this.isOk ? this.#value : defaultValue;
}
}
// Usage
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return Result.err(new Error(`HTTP ${res.status}`));
return Result.ok(await res.json());
} catch (e) {
return Result.err(e);
}
}
const result = await getUser(123)
.map(u => u.name) // Only runs if OK
.map(name => name.toUpperCase())
.orElse('Guest'); // Default if error
Global Error Handlers
// Browser
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
event.preventDefault(); // Prevent default logging
// Send to error tracking service
trackError(event.reason);
});
window.addEventListener('error', (event) => {
console.error('Uncaught error:', event.error);
trackError(event.error);
});
// Node.js
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
});
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
// Don't just ignore! Clean up and exit gracefully.
gracefulShutdown();
});
Express.js Error Handler
// Async handler wrapper for Express routes
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
// Routes using the wrapper
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user);
}));
// Global error handler (MUST be last middleware)
app.use((err, req, res, _next) => {
const isDev = process.env.NODE_ENV !== 'production';
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: err.message,
code: err.code,
...(isDev && { stack: err.stack }),
});
}
// Unknown errors — don't leak details in production
console.error('Unexpected error:', err);
res.status(500).json({
error: isDev ? err.message : 'Internal server error',
...(isDev && { stack: err.stack }),
});
});
Retry Logic
class RetryableError extends Error {
constructor(message, retryAfter = 1000) {
super(message);
this.name = 'RetryableError';
this.retryAfter = retryAfter;
}
}
async function retry(fn, options = {}) {
const {
maxAttempts = 3,
baseDelay = 1000,
maxDelay = 30000,
backoffFactor = 2,
shouldRetry = (err) => err instanceof RetryableError || err.code === 'ECONNRESET',
} = options;
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (!shouldRetry(error) || attempt === maxAttempts) throw error;
const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt - 1), maxDelay);
console.warn(`Attempt ${attempt}/${maxAttempts} failed, retrying in ${delay}ms...`);
await sleep(delay);
}
}
}
Circuit Breaker Pattern
class CircuitBreaker {
constructor(fn, options = {}) {
this.fn = fn;
this.threshold = options.threshold ?? 5; // Failures before opening
this.resetTimeout = options.resetTimeout ?? 60000; // ms before trying again
this.state = 'CLOSED'; // CLOSED → OPEN → HALF_OPEN
this.failures = 0;
this.lastFailureTime = null;
}
async execute(...args) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime >= this.resetTimeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await this.fn(...args);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
this.state = 'OPEN';
}
}
}
// Usage
const api = new CircuitBreaker(() => fetch('/api/data'));
await api.execute(); // Normal operation
// After 5 failures → throws "Circuit breaker is OPEN"
// After 60s → allows one request (HALF_OPEN)
How do you handle errors in your apps? Any patterns I missed?
Follow @armorbreak for more JavaScript content.
Top comments (0)