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
- Middleware runs in registration order; place parsing, security, and auth before routes and error handlers last.
- Express 5 forwards rejected promises from async handlers to error middleware; in Express 4, wrap async handlers or call
next(err). - Use one error-handling middleware (four arguments) that maps typed errors to status codes and hides internals.
- Check
res.headersSentbefore writing error responses; add a 404 handler before the error handler. - Set
trust proxyto match your actual proxy chain; limit body sizes and timeouts. - On SIGTERM, stop accepting connections, finish in-flight requests, close idle keep-alive connections, then exit.
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:
- Maps typed errors (validation, not found, conflict, unauthorized) to status codes and stable error codes.
- Hides internal details for 5xx responses: stack traces and database messages stay in logs.
- Logs server errors with the request ID and context; skips noisy logging for expected 4xx errors.
- Calls
next(err)ifres.headersSentis true, letting Express close the connection — you cannot change a response that has started streaming.
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
- Body size:
express.json()andexpress.urlencoded()default to 100 KB; set limits per route and stream large uploads instead of buffering them. - Timeouts: Node's
server.requestTimeoutandserver.headersTimeoutbound slow clients; set per-route timeouts for slow downstream calls. - Parameter and header limits guard against oversized or abusive requests.
Graceful Shutdown
Orchestrators send SIGTERM before killing a process. A graceful shutdown:
- Fails readiness checks so the load balancer stops sending new traffic.
- Calls
server.close()to stop accepting connections and wait for in-flight requests. - Closes idle keep-alive connections (
server.closeIdleConnections()), which otherwise keepclose()waiting. - Closes database pools, queues, and other resources.
- 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
- Express — The Node.js web framework
- Node.js — The runtime underneath Express
- Error Handling — Designing error responses across services
- Backend Service Lifecycle and Graceful Shutdown — Startup, readiness, and shutdown
- Rate Limiting — Limits that depend on correct client IPs
- Nginx Reverse Proxy — The proxy in front of many Express apps