Error Handling in Express

intermediate
14 min

Error Handling in Express

Every application fails sometimes: databases disconnect, clients send garbage, code has bugs. The difference between amateur and production-grade Express is what happens next. This lesson covers the error-handling middleware pattern, async errors, custom error classes, and a clean 404 strategy, giving you a template you can drop into any project.

The Error-Handling Middleware

Express recognizes a special middleware signature with four parameters. When any handler passes an error to next(err) or throws, Express skips all remaining normal middleware and jumps straight to it:

js
app.use((err, req, res, next) => { console.error(err.stack); const status = err.statusCode || 500; res.status(status).json({ error: status === 500 ? 'Internal server error' : err.message, }); });

Three rules: it must have exactly four parameters (Express detects the arity), it must be registered after all routes, and for 500s it should log the real error while sending the client a generic message. Leaking stack traces to users is both ugly and a security risk.

Triggering the Handler

Synchronous throws are caught automatically. In Express 5, rejected promises from async handlers are forwarded automatically too, which removes an entire class of historical boilerplate:

js
app.get('/api/books/:id', async (req, res) => { const book = await Book.findById(req.params.id); // rejection goes to the error handler if (!book) { const err = new Error('Book not found'); err.statusCode = 404; throw err; } res.json({ data: book }); });

If you are stuck on Express 4, wrap async handlers or use the express-async-errors package; otherwise unhandled rejections bypass your error middleware entirely.

A Custom Error Class

Attaching statusCode by hand gets repetitive. A tiny class makes intent explicit:

js
// errors/HttpError.js class HttpError extends Error { constructor(statusCode, message) { super(message); this.statusCode = statusCode; } } module.exports = HttpError;
js
const HttpError = require('./errors/HttpError'); app.get('/api/orders/:id', async (req, res) => { const order = await Order.findById(req.params.id); if (!order) throw new HttpError(404, 'Order not found'); res.json({ data: order }); });

Now the error handler can trust any HttpError to carry a safe, client-facing message, while everything else defaults to a generic 500.

Translating Library Errors

Databases throw their own error types. Translate the common ones in your error middleware so clients get accurate codes:

js
app.use((err, req, res, next) => { if (err.name === 'ValidationError') { return res.status(400).json({ error: err.message }); } if (err.name === 'CastError') { return res.status(400).json({ error: 'Invalid id format' }); } console.error(err); res.status(err.statusCode || 500).json({ error: err.statusCode ? err.message : 'Internal server error', }); });

The 404 Catch-All

A missing route is not an exception, just an unmatched request. Handle it with a normal middleware placed after every route but before the error handler:

js
app.use((req, res) => { res.status(404).json({ error: 'Route ' + req.originalUrl + ' not found' }); });

The final ordering of any Express app: parsers and loggers, routes, 404 catch-all, error handler. Memorize that sequence.

Key Takeaways

  • Error middleware has four parameters and must be registered last.
  • Express 5 forwards async rejections automatically; Express 4 needs wrappers.
  • Use a custom HttpError class to carry status codes with intent.
  • Translate ValidationError and CastError into 400 responses.
  • Log full errors server-side but never leak stack traces to clients.

Next lesson: Authentication with Sessions and JWT, where we control who can call these endpoints.

Error Handling in Express - Express.js | CodeYourCraft | CodeYourCraft