Express Error Handling
Robust error handling for Express apps: custom error classes, async wrappers, centralized error middleware, and environment-aware responses.
1. Custom Error Classes
// errors/AppError.js
class AppError extends Error {
constructor(message, statusCode = 500, code = null) {
super(message);
this.name = 'AppError';
this.statusCode = statusCode;
this.code = code; // e.g. 'INVALID_TOKEN'
this.isOperational = true; // distinguishes known errors from bugs
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}
class ValidationError extends AppError {
constructor(message, fields = {}) {
super(message, 422, 'VALIDATION_ERROR');
this.fields = fields;
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401, 'UNAUTHORIZED');
}
}
module.exports = { AppError, NotFoundError, ValidationError, UnauthorizedError };
2. Async Error Handling
// Utility wrapper — no try/catch needed in routes
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
// Or with a class for testing
class AsyncHandler {
static wrap(fn) {
return (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
}
}
// Usage
router.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user);
}));
// Without wrapper — manual try/catch
router.get('/items/:id', async (req, res, next) => {
try {
const item = await Item.findById(req.params.id);
res.json(item);
} catch (err) {
next(err); // must pass error to next()
}
});
3. Centralized Error Middleware
// middleware/errorHandler.js
const { AppError } = require('../errors/AppError');
function errorHandler(err, req, res, next) {
// Mongoose validation error
if (err.name === 'ValidationError') {
const fields = Object.fromEntries(
Object.entries(err.errors).map(([k, v]) => [k, v.message])
);
return res.status(422).json({ error: 'Validation failed', fields });
}
// JWT error
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Invalid token' });
}
// Operational / known errors
if (err.isOperational) {
const body = { error: err.message, code: err.code };
if (err.fields) body.fields = err.fields;
return res.status(err.statusCode).json(body);
}
// Unknown / programming errors
console.error('UNHANDLED ERROR:', err);
const message = process.env.NODE_ENV === 'production'
? 'Something went wrong'
: err.message;
res.status(500).json({ error: message });
}
module.exports = errorHandler;
// app.js — must be last middleware
app.use(errorHandler);
4. 404 Handler
// Place before errorHandler, after all routes
app.use((req, res, next) => {
next(new NotFoundError(`Route ${req.method} ${req.path}`));
});
// Or send directly
app.use((req, res) => {
res.status(404).json({
error: 'Route not found',
method: req.method,
path: req.path,
});
});
5. Unhandled Rejections & Exceptions
// Catch async errors that escape Express
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
// Graceful shutdown
server.close(() => process.exit(1));
});
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
process.exit(1); // always exit on uncaught exceptions
});
// Graceful shutdown on SIGTERM (e.g. Docker stop)
process.on('SIGTERM', () => {
console.log('SIGTERM received — shutting down gracefully');
server.close(() => {
console.log('HTTP server closed');
process.exit(0);
});
});
6. HTTP Status Code Reference
| Code | Name | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST (new resource) |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Malformed request / invalid input |
| 401 | Unauthorized | Authentication required |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Duplicate resource |
| 422 | Unprocessable Entity | Validation errors |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server error |
| 503 | Service Unavailable | Server temporarily down |