Skip to main content
Error handling is a critical part of any Express application. Express provides built-in mechanisms for handling both synchronous and asynchronous errors.

Error Handling Middleware

Error-handling middleware functions have four arguments instead of three: (err, req, res, next).
function errorHandler(err, req, res, next) {
  // Log the error
  console.error(err.stack);
  
  // Send error response
  res.status(500);
  res.send('Internal Server Error');
}

// Must be defined after all other routes
app.use(errorHandler);
Error-handling middleware must be defined after all other app.use() and route calls. Otherwise, it won’t catch errors from those routes.

Catching Errors

1

Synchronous Errors

Express automatically catches synchronous errors thrown in route handlers:
app.get('/', function(req, res) {
  // Express catches this automatically
  throw new Error('Something broke!');
});
2

Asynchronous Errors

For errors in asynchronous code, pass them to next():
app.get('/async', function(req, res, next) {
  // Simulate async operation
  process.nextTick(function() {
    next(new Error('Async error!'));
  });
});
3

Promise Rejections

For promises, catch rejections and pass to next():
app.get('/data', function(req, res, next) {
  fetchData()
    .then(data => res.json(data))
    .catch(next); // Pass errors to Express
});

Custom Error Types

Create custom errors with status codes:
app.get('/forbidden', function(req, res, next) {
  const err = new Error('Not allowed!');
  err.status = 403;
  next(err);
});

404 Handler

Handle 404 errors by adding a catch-all middleware before your error handler:
// 404 handler - must be after all routes
app.use(function(req, res, next) {
  res.status(404);
  
  res.format({
    html: function() {
      res.render('404', { url: req.url });
    },
    json: function() {
      res.json({ error: 'Not found' });
    },
    default: function() {
      res.type('txt').send('Not found');
    }
  });
});

// Error handler - must be last
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.render('error', { error: err });
});

Environment-Specific Handling

Disable verbose error details in production to avoid leaking sensitive information:
app.enable('verbose errors');

if (app.get('env') === 'production') {
  app.disable('verbose errors');
}

app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  
  // Only show stack trace in development
  res.render('error', {
    message: err.message,
    error: app.get('verbose errors') ? err : {}
  });
});

Best Practices

Define at least one error-handling middleware function to catch and handle errors gracefully.
In asynchronous code, always pass errors to next(err) rather than throwing them.
Use proper HTTP status codes (404, 403, 500, etc.) to indicate the type of error.
Always log errors for debugging, but don’t expose stack traces to users in production.

Build docs developers (and LLMs) love