Express Middleware and Error Propagation

Express applications are chains of middleware: functions that receive the request, do some work — parse a body, authenticate a user, log, route — and either respond or pass control to the next function. The model is simple, which is why Express remains the most widely used Node.js web framework, but its simplicity hides sharp edges: errors thrown in the wrong place crash the process or hang the request, middleware in the wrong order silently skips security checks, req.ip reports the load balancer instead of the client, and deploys drop in-flight requests.

This page covers the patterns that make Express services behave predictably: ordering middleware deliberately, propagating errors to one central handler, returning safe error responses, configuring proxy trust and request limits, and shutting down gracefully.

TL;DR

Quick Example

An Express 5 application with ordered middleware, typed errors, and a central error handler:

Core Concepts

Middleware Order

Express runs middleware and routes in the order they are registered, for paths that match:

A router mounted before authentication middleware is reachable without authentication — a common and serious bug. Mount sensitive routers explicitly with their guards: app.use("/admin", requireAdmin, adminRouter).

How Errors Propagate

In Express 4, wrap async handlers ((fn) => (req, res, next) => fn(req, res, next).catch(next)) or use a helper library. In both versions, errors in callbacks, event emitters, and timers escape the request: convert callback APIs to promises, or catch and pass errors to next manually.

Error-Handling Middleware

Error handlers have four parameters — (err, req, res, next) — which is how Express recognizes them. Keep one central handler that:

Express's default error handler prints stack traces when NODE_ENV is not production, so set it in production even with a custom handler. See Error Handling.

Proxies and Client Information

Behind a load balancer or reverse proxy, the TCP peer is the proxy. trust proxy tells Express which proxies to believe for X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host, which drive req.ip, req.protocol, and req.secure. Set it to the number of trusted hops or to specific addresses; true trusts any client-supplied header, letting attackers spoof IPs — which breaks rate limiting and audit logs. See Nginx Reverse Proxy and Rate Limiting.

Request Limits

Graceful Shutdown

Orchestrators send SIGTERM before killing a process. A graceful shutdown:

  1. Fails readiness checks so the load balancer stops sending new traffic.
  2. Calls server.close() to stop accepting connections and wait for in-flight requests.
  3. Closes idle keep-alive connections (server.closeIdleConnections()), which otherwise keep close() waiting.
  4. Closes database pools, queues, and other resources.
  5. Exits before the orchestrator's grace period ends, with a hard deadline as a fallback.

See Backend Service Lifecycle and Graceful Shutdown.

Best Practices

Centralize Error Responses

Define error classes and one handler, so every endpoint returns the same error format with a request ID.

Make Middleware Order Reviewable

Register global middleware in one place, in a documented order, and mount guarded routers explicitly.

Configure Proxy Trust Precisely

Match trust proxy to your real infrastructure and test that req.ip and req.protocol are correct in staging.

Handle Process-Level Failures

Log and exit on unhandledRejection and uncaughtException — the process state may be corrupt — and let the orchestrator restart it.

Common Mistakes

Unhandled Async Errors in Express 4

Leaking Internal Errors

trust proxy Set to true

FAQ

How does Express handle errors in async functions?

Express 5 forwards rejected promises returned by route handlers and middleware to error-handling middleware. Express 4 does not; wrap async handlers to catch rejections and call next(err).

How does Express know a middleware is an error handler?

By its signature: error-handling middleware takes four arguments, (err, req, res, next). Express calls it only when an error has been passed along.

What does trust proxy do in Express?

It tells Express which proxies to trust for forwarded headers, so req.ip, req.protocol, and req.secure reflect the original client request. Configure it to match your actual proxy chain.

Why does my error handler say headers were already sent?

The response had started (for example, streaming) before the error occurred. Check res.headersSent and delegate to next(err) so Express closes the connection.

How do I shut down an Express server gracefully?

On SIGTERM, fail readiness checks, call server.close() to finish in-flight requests, close idle keep-alive connections, release resources, and exit before the orchestrator's deadline.

Related Topics

References