- Express.js Basics
- Express.js HOME
- Express.js Introduction
- Express.js Installation
- Express.js Basic App
- Express.js Routing
- Basics Routing
- Route Parameters
- Handling Query Strings
- Router Middleware
- Middleware
- What is Middleware?
- Application-Level Middleware
- Router-Level Middleware
- Built-In Middleware
- Error-Handling Middleware
- Third-Party Middleware
- Express.js HTTP
- Handling GET Requests
- Handling POST Requests
- Handling PUT Requests
- Handling DELETE Requests
- Templating Engines
- Using Templating Engines
- Setting Up EJS
- Setting Up Handlebars
- Setting Up Pug
- Request/Response
- Request Object
- Response Object
- Handling JSON Data
- Handling Form Data
- Static Files
- Serving Static Files
- Setting Up Static Folders
- Managing Assets
- Express.js Advanced
- Middleware Stack
- CORS in Express.js
- JWT Authentication
- Session Handling
- File Uploads
- Error Handling
- Databases
- Express.js with MongoDB
- MongoDB CRUD Operations
- Express.js with MySQL
- MySQL CRUD Operations
- Deployment
- Deploying Express.js Apps to Heroku
- Deploying Express.js Apps to AWS
- Deploying Express.js Apps to Vercel
Express.js Error Handling Middleware
In a production-grade web application, things will inevitably go wrong—a database connection might time out, a user might request a resource that doesn't exist, or an external API might fail. Error handling middleware is your safety net. It allows you to intercept these failures, log them for debugging, and return a clean, professional response to the user instead of letting the application crash or hang.
Key Features of Error Handling Middleware
- Centralized Error Handling: Instead of writing
try-catchblocks in every single route to send responses, you can delegate that responsibility to a single piece of middleware at the end of your pipeline. - Customizable Responses: You can decide exactly what the client sees. For example, you might send a full stack trace during local development but a generic "Internal Server Error" message in production to keep your server details secure.
- Handles Synchronous and Asynchronous Errors: Express provides mechanisms to catch errors from standard functions, Promises, and
async/awaitblocks, ensuring your app stays resilient regardless of the coding style.
Steps to Implement Error Handling Middleware
Basic Error Handling Middleware
Express recognizes error-handling middleware by the number of arguments it accepts. While standard middleware uses three arguments (req, res, next), error-handling middleware must have exactly four: err, req, res, and next.
Example:
const express = require('express');
const app = express();
// A route that simulates a failure
app.get('/error', (req, res, next) => {
const error = new Error('Database connection failed!');
// When you pass an argument to next(), Express skips all remaining
// non-error middleware and goes straight to the error handler.
next(error);
});
// The error handling middleware - MUST have 4 arguments
app.use((err, req, res, next) => {
console.error('Logging Error:', err.message);
// Set the status code (default to 500 if not specified)
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
res.status(statusCode).json({
message: err.message,
// Only show stack trace in development mode
stack: process.env.NODE_ENV === 'production' ? '🥞' : err.stack,
});
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
next). Even if you don't use the next object inside your error handler, you must include it in the function signature. If you only provide three arguments, Express will treat it as regular middleware, and it won't catch your errors.
Error Handling for Asynchronous Code
In Express 4.x, errors thrown in synchronous code are caught automatically. however, for asynchronous code (like database queries), you must manually catch the error and pass it to next().
Example with Promises:
app.get('/async-error', (req, res, next) => {
// Simulating a database call that fails
fetchDataFromDB()
.then(data => res.json(data))
.catch(next); // This is shorthand for .catch(err => next(err))
});
Example with async/await:
app.get('/async-await-error', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
throw new Error('User not found');
}
res.json(user);
} catch (err) {
// Essential: If you don't call next(err), the request will hang
next(err);
}
});
async functions will not be caught by your error middleware automatically. You must wrap your logic in try/catch or use a wrapper library like express-async-errors.
Custom Error Types
Using generic errors is fine for small projects, but for larger apps, creating custom error classes helps you distinguish between "Operational Errors" (like an invalid password) and "Programmer Errors" (like a typo in a variable name).
Example:
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // Useful to distinguish known errors from bugs
}
}
app.get('/protected-route', (req, res, next) => {
const isAuthorized = false;
if (!isAuthorized) {
return next(new AppError('You are not authorized to access this', 403));
}
res.send('Welcome, Admin!');
});
app.use((err, req, res, next) => {
const status = err.statusCode || 500;
res.status(status).send({
status: 'fail',
message: err.message
});
});
Default Error Handling Middleware
Express comes with a built-in error handler that takes care of any errors you might have missed. However, once you define your own custom error middleware, it replaces the default one.
app.use() and route definitions. This ensures that any error occurring in any route eventually "bubbles down" to the handler.
Error Handling for Specific Routes
Sometimes, you want specific logic for certain parts of your app. For example, your API might return JSON errors, while your website frontend should return a rendered HTML "404" page.
Example:
const apiRouter = express.Router();
// Error handler specifically for API routes
apiRouter.use((err, req, res, next) => {
console.error('API Error:', err);
res.status(400).json({ error: 'API Request failed', details: err.message });
});
apiRouter.get('/data', (req, res, next) => {
next(new Error('Data unavailable'));
});
app.use('/api/v1', apiRouter);
Example of Complete Error Handling Setup
Here is a real-world pattern that includes body parsing, a route, and a robust global error handler.
const express = require('express');
const app = express();
app.use(express.json());
// 1. Regular Route
app.get('/user/:id', async (req, res, next) => {
try {
// Logic here
if (req.params.id === '0') throw new Error('Invalid User ID');
res.send('User Data Found');
} catch (err) {
next(err);
}
});
// 2. Handle 404 (Route Not Found)
// This is not an error handler, but a catch-all for missing routes
app.use((req, res, next) => {
res.status(404).send('Sorry, we could not find that page.');
});
// 3. Global Error Handler
app.use((err, req, res, next) => {
const status = err.status || 500;
console.error(`[Error] ${req.method} ${req.url}: ${err.message}`);
res.status(status).json({
success: false,
message: err.message || 'Internal Server Error'
});
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
Summary
Error handling middleware in Express is a powerful tool that transforms your application from a fragile script into a robust service. By implementing a centralized error handler with the four-argument signature (err, req, res, next), you ensure that your application can recover gracefully from failures. Remember to handle both synchronous and asynchronous errors, use custom error classes for better organization, and always place your error handlers at the end of the middleware stack to catch everything that falls through.