Skip to content

ExpressJS - Error handling

Robust error handling is a critical part of any production application. It prevents the server from crashing, helps in debugging, and provides meaningful feedback to users without exposing sensitive implementation details.

In Express, error handling is done using a special type of middleware. This middleware function has four arguments instead of the usual three: (err, req, res, next). This signature is how Express identifies it as an error-handling middleware.

This middleware should be defined last, after all other app.use() and route calls.

app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});

You can trigger the error-handling middleware in two ways:

  • Synchronous Errors: If an error is thrown in synchronous code within a route handler or middleware, Express will catch it and pass it to the error handler automatically.
  • Asynchronous Errors: For errors in asynchronous code (like database queries or API calls), you must catch them and pass them to next().

In this example, we manually create an error and pass it to next(err). This skips all subsequent non-error-handling middleware and routes and goes directly to our error handler.

import express from 'express';
const app = express();
app.get('/problem', (req, res, next) => {
// Simulate an error
const err = new Error('This is a simulated error.');
err.status = 418; // You can add custom properties
// Pass the error to the next middleware
next(err);
});
// Define other routes and middleware here...
app.get('/', (req, res) => res.send('Welcome!'));
// The centralized error handler (must be last)
app.use((err, req, res, next) => {
console.error('Error occurred:', err.message);
// Set a default status code if none is provided
const statusCode = err.status || 500;
res.status(statusCode).json({
error: {
message: err.message || 'An internal server error occurred.'
}
});
});
app.listen(3000, () => console.log('Server running on port 3000'));

When you visit http://localhost:3000/problem, the browser will display a JSON response with a 418 status code:

{
"error": {
"message": "This is a simulated error."
}
}

Handling errors in async/await functions requires a try...catch block to pass the error to next().

import { findUserById } from './db'; // Assume this is an async DB function
app.get('/users/:id', async (req, res, next) => {
try {
const user = await findUserById(req.params.id);
if (!user) {
// Create a specific error for 'not found'
const err = new Error('User not found');
err.status = 404;
return next(err);
}
res.json(user);
} catch (error) {
// Catch database errors or other exceptions
next(error);
}
});

Modern Development Tip: Libraries like express-async-errors can automatically handle this for you. By simply importing the library, you can eliminate the need for try...catch blocks in your async route handlers.

  • Centralize Error Handling: Use a single, centralized error-handling middleware for consistent error responses.
  • Don’t Leak Implementation Details: In a production environment, never send the full error stack trace to the client. Log it for debugging but send a generic, user-friendly message.
  • Create Custom Error Classes: For more structured error handling, extend the built-in Error class to create custom error types (e.g., ApiError, NotFoundError) that can hold a status code and other metadata.
  • Handle Uncaught Exceptions: For ultimate stability, listen for process.on('uncaughtException') and process.on('unhandledRejection') to catch errors outside the Express request-response cycle. Log the error and gracefully shut down the server.