Express Error Handling Middleware in Node.js
This article explains how to design and implement a robust, centralized error-handling system in a Node.js Express application. You will learn how to create a custom error class, build a centralized error-handling middleware, and gracefully handle asynchronous errors without cluttering your route handlers with repetitive try-catch blocks.
1. Create a Custom Error Class
To distinguish between operational errors (known issues like
validation failures or resource shortages) and programmer errors (bugs),
create a custom error class that extends the built-in JavaScript
Error class. This allows you to attach properties like HTTP
status codes.
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;2. Build the Centralized Error Middleware
Express recognizes error-handling middleware by its signature. It
must accept exactly four arguments: (err, req, res, next).
Place this middleware at the end of your middleware stack to catch all
errors passed via next().
const globalErrorHandler = (err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
} else {
// Production mode: do not leak leak stack traces or internal details
if (err.isOperational) {
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
} else {
// Log generic/programming errors to the console or log service
console.error('ERROR 💥', err);
res.status(500).json({
status: 'error',
message: 'Something went wrong!'
});
}
}
};
module.exports = globalErrorHandler;3. Handle Asynchronous Errors Automatically
Writing try/catch blocks in every asynchronous route
handler leads to repetitive code. To solve this, implement a wrapper
function that catches rejected promises and forwards them to the
next function.
const catchAsync = fn => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};You can then wrap your controller functions like this:
const getUser = catchAsync(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(new AppError('No user found with that ID', 404));
}
res.status(200).json({ status: 'success', data: { user } });
});4. Register the Middleware in the Application
To ensure Express catches all errors, register the global error
handler as the very last middleware function in your app.js
or server setup file, after all your route definitions.
const express = require('express');
const AppError = require('./AppError');
const globalErrorHandler = require('./globalErrorHandler');
const app = express();
app.use(express.json());
// Example Route
app.get('/api/users/:id', getUser);
// Handle undefined routes
app.all('*', (req, res, next) => {
next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404));
});
// Mount the global error handling middleware
app.use(globalErrorHandler);
app.listen(3000, () => {
console.log('Server is running on port 3000');
});