Error Handling in Node.js: The Missing Guide
Most Node.js tutorials skip error handling. Here's what they don't tell you.
The Three Types of Errors
// 1. Operational Errors (expected, handle them)
// - File not found, network timeout, invalid input
// - These ARE going to happen. Plan for them.
// 2. Programmer Errors (bugs, fix them)
// - TypeError, ReferenceError, logic errors
// - These should NOT happen. Fix the code.
// 3. System Errors (infrastructure, retry them)
// - Out of memory, connection refused, DNS failure
// - Often transient. Retry with backoff.
Synchronous Error Handling
// Use try/catch for synchronous code
function parseConfig(filePath) {
try {
const raw = fs.readFileSync(filePath, 'utf8');
return JSON.parse(raw);
} catch (err) {
if (err.code === 'ENOENT') {
console.error(`Config file not found: ${filePath}`);
return getDefaultConfig();
}
if (err instanceof SyntaxError) {
console.error(`Invalid JSON in ${filePath}: ${err.message}`);
throw new ConfigError('Malformed config file', { filePath });
}
throw err; // Unknown error, let it bubble
}
}
Async Error Handling (The Right Way)
❌ Wrong: Callbacks
// Unhandled rejection if readFile fails
fs.readFile('data.json', (err, data) => {
const parsed = JSON.parse(data); // What if data is malformed?
});
✅ Right: Promises with try/catch
async function loadData() {
try {
const raw = await fs.promises.readFile('data.json', 'utf8');
return JSON.parse(raw);
} catch (err) {
if (err.code === 'ENOENT') {
return getDefaultData();
}
throw new DataError('Failed to load data', { cause: err });
}
}
✅ Right: Express middleware
// Async errors need special handling in Express!
app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json({ data: user });
} catch (err) {
next(err); // Pass to error handler!
}
});
// Global error handler (MUST have 4 parameters)
app.use((err, req, res, _next) => {
console.error(`[ERR] ${req.method} ${req.path}:`, err);
// Don't leak stack traces in production
const message = process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message;
res.status(err.statusCode || 500).json({
error: {
code: err.code || 'INTERNAL_ERROR',
message,
...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
}
});
});
Custom Error Classes
// Base application error
class AppError extends Error {
constructor(message, { statusCode = 500, code = 'APP_ERROR', details = null } = {}) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.code = code;
this.details = details;
Error.captureStackTrace(this, this.constructor);
}
}
// Specific error types
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} not found`, {
statusCode: 404,
code: 'NOT_FOUND',
details: { resource, id }
});
}
}
class ValidationError extends AppError {
constructor(errors) {
super('Validation failed', {
statusCode: 422,
code: 'VALIDATION_ERROR',
details: errors // [{ field: 'email', issue: 'Invalid format' }]
});
}
}
class AuthError extends AppError {
constructor(message = 'Authentication required') {
super(message, { statusCode: 401, code: 'AUTH_ERROR' });
}
}
class RateLimitError extends AppError {
constructor(retryAfter) {
super('Too many requests', {
statusCode: 429,
code: 'RATE_LIMITED',
details: { retryAfter }
});
}
}
// Usage
if (!user) throw new NotFoundError('User', id);
if (!isValidEmail(email)) throw new ValidationError([{ field: 'email', issue: 'Invalid format' }]);
Retry with Exponential Backoff
async function withRetry(fn, { maxAttempts = 3, baseDelay = 1000, jitter = true } = {}) {
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
// Don't retry client errors (4xx)
if (err.statusCode >= 400 && err.statusCode < 500) throw err;
if (attempt === maxAttempts) throw err;
const delay = baseDelay * Math.pow(2, attempt - 1);
const jitterAmount = jitter ? Math.random() * 1000 : 0;
console.warn(`Attempt ${attempt}/${maxAttempts} failed: ${err.message}. Retrying in ${Math.round(delay + jitterAmount)}ms`);
await new Promise(resolve => setTimeout(resolve, delay + jitterAmount));
}
}
throw lastError;
}
// Usage
const data = await withRetry(
() => fetch('https://api.example.com/data').then(r => r.json()),
{ maxAttempts: 5, baseDelay: 500 }
);
Graceful Shutdown
const server = app.listen(3000);
// Track active connections
const connections = new Set();
server.on('connection', (conn) => {
connections.add(conn);
conn.on('close', () => connections.delete(conn));
});
// Handle shutdown signals
function shutdown(signal) {
console.log(`\n${signal} received. Shutting down gracefully...`);
// Stop accepting new connections
server.close(() => {
console.log('HTTP server closed');
process.exit(0);
});
// Force exit after timeout
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
// Handle uncaught exceptions (last resort!)
process.on('uncaughtException', (err) => {
console.error('UNCAUGHT EXCEPTION:', err);
// Log and restart — don't try to continue
shutdown('UNCAUGHT_EXCEPTION');
});
process.on('unhandledRejection', (reason, promise) => {
console.error('UNHANDLED REJECTION:', reason);
// Log it, but don't crash for unhandled rejections (Node 15+ changed this)
});
The Error Handling Checklist
- [ ] Every
awaitin atry/catchor.catch() - [ ] Express async routes pass errors to
next(err) - [ ] Global error handler doesn't leak stack traces in production
- [ ] Custom error classes for different error types
- [ ] Network calls use retry with backoff
- [ ] Database queries handle connection errors
- [ ] Input validation before processing
- [ ] Graceful shutdown on SIGTERM/SIGINT
- [ ]
uncaughtExceptionandunhandledRejectionhandlers - [ ] Error logging with context (request ID, user, path)
- [ ] Client-friendly error messages (not raw stack traces)
What's your approach to error handling? Any patterns I missed?
Follow @armorbreak for more Node.js guides.
Top comments (0)