By default, if an error occurs in your synchronous code, Express will catch it and respond with a basic HTML error page. For an API, this isn't helpful. We need a robust, centralized way to catch all errors and respond with a consistent JSON format.
The Default Error Handler is Not Enough
Without a custom handler, your API might reveal stack traces in production (a security risk) or just hang on asynchronous errors. We need to take control.
Creating a Custom Error-Handling Middleware
Express treats any middleware with four arguments as an error-handling middleware. Its signature must be (err, req, res, next).
This middleware should be defined last, after all other app.use() and route calls. This makes it the final destination for any errors that occur in the preceding handlers.
Here’s a basic custom error handler:
JavaScript
const express = require('express');
const app = express();
// --- Routes ---
app.get('/', (req, res) => {
res.send('Hello!');
});
app.get('/error', (req, res, next) => {
// We can create an error and pass it to next()
const err = new Error('This is a test error!');
err.status = 418; // I'm a teapot
next(err); // Pass the error to the error-handling middleware
});
// --- Custom Error Handling Middleware ---
// It has 4 arguments, making it an error handler.
function errorHandler(err, req, res, next) {
// Set a default status code if none is provided
const statusCode = err.status || 500;
console.error(err.stack); // Log the full error for debugging
// Send a structured JSON response
res.status(statusCode).json({
status: 'error',
statusCode: statusCode,
message: err.message || 'Something went wrong on the server.',
});
}
// Register the error handler LAST.
app.use(errorHandler);
app.listen(3000, () => console.log('Server running...'));
How It Works
- Triggering the Handler: In any regular route or middleware, if you call next(someErrorObject), Express will skip all subsequent non-error-handling middleware and route handlers and jump straight to your custom error handler.
- Handling Asynchronous Errors: In async/await functions, you must wrap your code in a try...catch block and call next(error) from the catch block. Libraries like express-async-errors can automate this for you.
- Structured Responses: Notice our response format: { "status": "error", "statusCode": 500, "message": "..." }. Using a consistent structure for both successful and error responses makes your API predictable and easier for frontend developers to consume.
Creating Custom Error Classes
For more advanced applications, you can create your own error classes to handle different types of errors (e.g., NotFoundError, ValidationError, AuthenticationError) and check for them in your handler using instanceof.
JavaScript
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'NotFoundError';
this.status = 404;
}
}
// In a route:
// next(new NotFoundError('Resource not found'));
// In the error handler:
// if (err instanceof NotFoundError) { ... }
This centralized approach keeps your route handlers clean and puts all your error logic in one predictable place.