Error Handling in JavaScript: Beyond try/catch
Most developers only scratch the surface. Here's the full picture.
The Basics (Quick Refresher)
try {
// Code that might throw
riskyOperation();
} catch (error) {
// Handle the error
console.error('Something went wrong:', error.message);
} finally {
// Always runs (cleanup, close connections)
cleanup();
}
Custom Error Classes
// Base application error
class AppError extends Error {
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.code = code; // Machine-readable code for API responses
this.timestamp = new Date().toISOString();
Error.captureStackTrace(this, this.constructor);
}
}
// Specific error types
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} with id '${id}' not found`, 404, 'NOT_FOUND');
this.resource = resource;
this.id = id;
}
}
class ValidationError extends AppError {
constructor(fields) {
super('Validation failed', 422, 'VALIDATION_ERROR');
this.fields = fields; // { email: 'Invalid format', name: 'Required' }
}
toJSON() {
return {
error: { ...this, fields: this.fields },
};
}
}
class RateLimitError extends AppError {
constructor(retryAfter) {
super('Too many requests', 429, 'RATE_LIMITED');
this.retryAfter = retryAfter;
}
}
// Usage:
function getUser(id) {
const user = database.find(id);
if (!user) throw new NotFoundError('User', id);
return user;
}
Async Error Handling Patterns
// Pattern 1: try/catch in async function
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new NotFoundError('User', id);
return await response.json();
} catch (error) {
if (error instanceof NotFoundError) {
showNotFoundPage();
return null;
}
// Re-throw unexpected errors to be handled higher up
throw error;
}
}
// Pattern 2: Wrapper that returns [data, error] tuple
async function tryCatch(promise) {
try {
const data = await promise;
return [data, null];
} catch (error) {
return [null, error];
}
}
// Usage — no try/catch needed!
const [user, error] = await tryCatch(fetchUser(123));
if (error) {
handleError(error);
return;
}
console.log(user.name);
// Pattern 3: Higher-order function wrapper
const withErrorHandler = (fn) => async (...args) => {
try {
return await fn(...args);
} catch (error) {
logError(error);
return formatErrorResponse(error);
}
};
app.get('/api/users/:id', withErrorHandler(async (req, res) => {
const user = await fetchUser(req.params.id);
res.json(user); // If fetchUser throws, withErrorHandler catches it
}));
Global Error Handling
// Node.js uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('UNCAUGHT EXCEPTION:', error);
// Log to external service
logToService(error);
// Don't just crash! But also don't keep running blindly.
// Best practice: Graceful shutdown
gracefulShutdown();
// Note: After uncaughtException, your app state is uncertain.
// Restart is usually the safest option.
});
// Unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('UNHANDLED REJECTION:', reason);
logToService({ type: 'unhandledRejection', reason });
});
// Browser global handler
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global error:', message, source, lineno);
reportToErrorTrackingService({ message, source, lineno, colno, stack: error?.stack });
};
window.addEventListener('unhandledrejection', (event) => {
event.preventDefault(); // Prevent default logging
reportToErrorTrackingService({ reason: String(event.reason) });
});
Express.js Error Handling
// 404 handler (must be after all routes)
app.use((req, res, _next) => {
res.status(404).json({
error: { code: 'NOT_FOUND', message: `${req.method} ${req.path} not found` },
});
});
// Centralized error handler (MUST have 4 parameters!)
app.use((err, req, res, _next) => {
console.error(err.stack);
// Known app errors → send structured response
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: { code: err.code, message: err.message },
});
}
// JSON parse error
if (err.type === 'entity.parse.failed') {
return res.status(400).json({
error: { code: 'INVALID_JSON', message: 'Malformed JSON in request body' },
});
}
// Unknown errors → generic message in production
const statusCode = err.statusCode || 500;
const message = process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message;
res.status(statusCode).json({
error: { code: 'INTERNAL_ERROR', message },
...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
});
});
Error Reporting & Monitoring
// Simple error reporting service
class ErrorReporter {
constructor(serviceUrl, apiKey) {
this.serviceUrl = serviceUrl;
this.apiKey = apiKey;
this.queue = [];
this.flushInterval = setInterval(() => this.flush(), 10000);
}
async report(error, context = {}) {
const entry = {
message: error.message || String(error),
stack: error.stack,
context: {
url: typeof window !== 'undefined' ? window.location.href : undefined,
userId: context.userId,
route: context.route,
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV,
...context,
},
};
this.queue.push(entry);
if (this.queue.length >= 10) {
this.flush(); // Flush immediately if queue is large
}
}
async flush() {
if (this.queue.length === 0) return;
const entries = [...this.queue];
this.queue = [];
try {
await fetch(this.serviceUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiKey },
body: JSON.stringify(entries),
});
} catch (e) {
// Can't report errors? Put them back
this.queue.unshift(...entries);
}
}
}
// Usage:
const reporter = new ErrorReporter('https://errors.example.com/api/report', 'api-key-123');
try {
riskyOperation();
} catch (error) {
await reporter.report(error, { userId: user.id, route: '/dashboard' });
// Show user-friendly message
showToast('Something went wrong. We\'ve been notified.');
}
Error Boundaries (React)
// React class component for catching rendering errors
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
// Log to error service
logErrorToService(error, { componentStack: info.componentStack });
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Wrap your app or specific components:
<ErrorBoundary>
<Dashboard />
</ErrorBoundary>
What's the trickiest error handling scenario you've faced?
Follow @armorbreak for more JavaScript content.
Top comments (0)