On this page
Error Handling
Robust error handling prevents crashes and returns meaningful responses to clients.
Synchronous Errors
try {
JSON.parse('invalid json');
} catch (err) {
console.error('Parse error:', err.message);
}
Async Errors with async/await
async function fetchUser(id) {
try {
const user = await User.findById(id);
if (!user) throw new Error('User not found');
return user;
} catch (err) {
throw err; // Re-throw or transform
}
}
Custom Error Classes
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, 404);
}
}
// Usage
throw new NotFoundError('User');
Express Global Error Handler
// Async wrapper
const catchAsync = (fn) => (req, res, next) => {
fn(req, res, next).catch(next);
};
app.get('/users/:id', catchAsync(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user);
}));
// Global handler (must be last middleware)
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Internal Server Error';
if (process.env.NODE_ENV === 'development') {
console.error(err.stack);
}
res.status(statusCode).json({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
Unhandled Rejections and Exceptions
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
// Log and gracefully shutdown
process.exit(1);
});
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
process.exit(1);
});
404 for Unknown Routes
app.all('*', (req, res, next) => {
next(new NotFoundError(`Route ${req.originalUrl}`));
});
Validation Errors
Return 400 with details:
if (!req.body.email) {
throw new AppError('Email is required', 400);
}
Always distinguish operational errors (expected, like 404) from programming errors (bugs) — log the latter, return safe messages to clients.
Error-First Callback Pattern
Legacy Node APIs use callbacks where the first argument is an error:
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) {
console.error('Failed to read config:', err.message);
return;
}
const config = JSON.parse(data);
});
Wrap callbacks with util.promisify for async/await:
import { promisify } from 'util';
const readFile = promisify(fs.readFile);
const data = await readFile('config.json', 'utf8');
Centralized Error Logging
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.File({ filename: 'error.log', level: 'error' })]
});
app.use((err, req, res, next) => {
logger.error({ message: err.message, stack: err.stack, url: req.url });
res.status(err.statusCode || 500).json({ error: err.message });
});
Graceful Shutdown
const server = app.listen(3000);
process.on('SIGTERM', () => {
console.log('SIGTERM received — closing server');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});
Kubernetes sends SIGTERM before killing pods — handle it to finish in-flight requests.
Error Handling Checklist
- All async route handlers wrapped with
catchAsyncor try/catch - Global error middleware registered last
-
unhandledRejectionanduncaughtExceptionhandlers configured - Operational vs programming errors distinguished
- Stack traces hidden in production responses