Facebook Pixel

Express Error-Handling Middleware Explained

Error handling is what separates a learning server from a production one. Here is how it works.

Express Error-Handling Middleware Explained

Error handling in Express has a special middleware shape. Here is how to use it.

The Shape

An error-handling middleware has four parameters (Express uses arity to detect it):

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || 'Something went wrong'
  });
});

Register it last, after all routes. Express jumps to it when a middleware or handler calls next(err) or throws.

Throwing Errors in Handlers

router.get('/users/:id', async (req, res, next) => {
  const user = await User.findById(req.params.id);
  if (!user) return next(new Error('User not found'));
  res.json(user);
});

Or throw (with async wrapper or Express 5):

if (!user) throw new ApiError(404, 'User not found');

Custom ApiError Class

class ApiError extends Error {
  constructor(status, message) {
    super(message);
    this.status = status;
  }
}

Now you can throw new ApiError(404, 'User not found') and the error handler reads err.status and err.message.

Catching Async Errors

Express 4 does not catch promise rejections. Use a wrapper:

const asyncHandler = fn => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

router.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) throw new ApiError(404, 'User not found');
  res.json(user);
}));

Express 5 catches async errors automatically.

Don't Leak Stack Traces

In production, do not send the stack:

res.status(err.status || 500).json({
  error: process.env.NODE_ENV === 'production'
    ? (err.expose ? err.message : 'Something went wrong')
    : err.message,
  stack: process.env.NODE_ENV === 'production' ? undefined : err.stack
});

Use an expose flag on ApiError for errors that are safe to show (validation, not found). For 500s, send a generic message.

Logging

Log errors with a real logger (winston, pino), not console.error:

app.use((err, req, res, next) => {
  logger.error({ err, path: req.path, method: req.method, userId: req.user?.id });
  res.status(err.status || 500).json({ error: err.message });
});

For real apps, integrate Sentry or similar for error alerts.

Multiple Error Handlers

You can have multiple error-handling middlewares, e.g., one for known ApiErrors and one for unknown errors:

app.use((err, req, res, next) => {
  if (err instanceof ApiError) return res.status(err.status).json({ error: err.message });
  next(err);
});
app.use((err, req, res, next) => {
  logger.error({ err });
  res.status(500).json({ error: 'Something went wrong' });
});

The Takeaway

Express error handling uses a four-parameter middleware. Throw or call next(err) in handlers. Use a custom ApiError class with status and message. Wrap async handlers to catch promise rejections. Never leak stack traces in production. Log with a real logger and integrate Sentry. Register error handlers last.

It has four parameters (err, req, res, next). Express detects this arity and uses it for errors. Register it last. In handlers, throw or call next(err). Express then jumps to the error handler.

Use an asyncHandler wrapper: Promise.resolve(fn(req, res, next)).catch(next). This catches promise rejections and passes them to the error handler. Express 5 catches async errors automatically.

A subclass of Error with a status and message. Use it to throw new ApiError(404, 'User not found'). The error handler reads err.status and err.message, so errors are consistent and typed.

Stack traces expose internal file paths, library versions, and logic. Attackers can use them to find weaknesses. In production, send a safe message; log the full error internally. Use an expose flag on ApiError for safe-to-show errors.

Yes. For example, one for known ApiErrors that returns their status and message, and another for unknown 500s that logs and returns a generic message. Call next(err) from the first to pass to the second when the error is not an ApiError.

Ready to master Node.js completely?

Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.

Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.