How to handle errors in Express.js?
Error Handling in Express.js
Express has a built-in error-handling mechanism. Proper error handling is essential to avoid crashes and return meaningful responses to clients.
Synchronous Errors
Express automatically catches errors thrown in synchronous route handlers:
app.get('/user/:id', (req, res) => {
if (isNaN(req.params.id)) {
throw new Error('ID must be a number'); // Express catches this
}
res.json({ id: req.params.id });
});Asynchronous Errors
For async code, you must either use try/catch or pass errors to next():
// Option 1: try/catch + next(err)
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
} catch (err) {
next(err); // pass to error handler
}
});
// Option 2: asyncHandler wrapper
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
}));Express 5 (currently in RC) handles async errors automatically.
Error-Handling Middleware
The error handler has 4 parameters — Express identifies it by the err argument:
// Must be registered LAST, after all routes
app.use((err, req, res, next) => {
console.error(err.stack);
const status = err.statusCode || err.status || 500;
const message = err.message || 'Internal Server Error';
res.status(status).json({
success: false,
error: message,
// only show stack in development
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});Custom Error Classes
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
class UnauthorizedError extends AppError {
constructor() {
super('Unauthorized', 401);
}
}
// Usage in routes
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);
}));404 Handler
Catch unmatched routes — register it after all routes but BEFORE the error handler:
// After all routes
app.use((req, res, next) => {
next(new NotFoundError(`Route ${req.method} ${req.path}`));
});
// Error handler (last)
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).json({ error: err.message });
});Full Error Handling Setup
const express = require('express');
const app = express();
app.use(express.json());
// Routes
app.use('/api/users', usersRouter);
// 404 — must come after all routes
app.use((req, res, next) => {
res.status(404).json({ error: 'Route not found' });
});
// Global error handler — must be last
app.use((err, req, res, next) => {
const status = err.statusCode || 500;
res.status(status).json({
error: err.message,
...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
});
});
app.listen(3000);Summary
Register error handlers with 4 parameters and place them last in the middleware chain. Use next(err) to forward errors from async code, and create custom error classes to carry HTTP status codes and contextual messages.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.