To log an Express error with its request ID and stack, do three things. Create a request-scoped store with AsyncLocalStorage as early as possible. Make sure every failure, including async ones, reaches a four-argument error middleware. Log the original Error there, with the ID, on the server only, and send the client a generic body. console.error(err.stack) alone gives you a stack but no request context. The context has to be attached or propagated separately.
These are implementation patterns based on the official Express and Node.js documentation. They have not been run against a live application, so check them against your Express major version and your logging setup.
Step 1: Establish request context at the entry point
Node’s AsyncLocalStorage (from node:async_hooks) lets you run a callback with a store that stays available to asynchronous operations created inside it. Node’s asynchronous context tracking docs describe run() and getStore(). Register this middleware before your routes:
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';
export const requestContext = new AsyncLocalStorage();
app.use((req, res, next) => {
const requestId = randomUUID();
requestContext.run({ requestId }, () => next());
});
Choices that matter
- Prefer
run()overenterWith(). Node’s docs favorrun(), becauseenterWith()can persist across later synchronous event-handler work. - Handle a missing store.
getStore()returnsundefinedoutside a context started withrun()orenterWith(). Use optional chaining in any logger that can fire outside a request, such as at startup or in background jobs. - Decide your incoming ID policy. The example generates its own ID. If you accept an upstream ID instead, validate its format and length, and consider keeping it as a separate field from your internal ID. Don’t let a caller-supplied value act as anything with authority. This is an application policy choice that the cited pages don’t prescribe.
Step 2: Make sure errors reach the error handler
The forwarding rules depend on your Express major version. Check which one is installed before copying code.
#1 Best Overall
| Situation | Express 4 | Express 5 |
|---|---|---|
| Synchronous throw in a route | Caught by Express | Caught by Express |
| Rejected Promise from an async handler | Forward it yourself: try/catch with next(err), or .catch(next) |
Forwarded automatically if the handler returns the Promise (async functions always do) |
| Error-first callback | next(err) |
next(err) |
| Promise started but not returned | Express can’t see it; forward explicitly | Express can’t see it; forward explicitly |
Express 5
The Express 5.x guide states: “Route handlers and middleware that return a Promise call next(value) automatically when they reject or throw an error, and async functions always return a Promise, so their errors reach Express with no extra work.” Return your Promise chains. Where there is no Promise to return, such as timers or other work without an error-first callback, catch the error inside that operation and call next(err).
Express 4
This is why an async error in Express 4 bypasses your error middleware. The Express 4.x guide requires you to forward asynchronous failures yourself:
Rank #2
app.get('/orders/:id', async (req, res, next) => {
try {
res.json(await loadOrder(req.params.id));
} catch (err) {
next(err);
}
});
// or with a returned Promise
app.get('/orders', (req, res, next) => {
listOrders().then(rows => res.json(rows)).catch(next);
});
In both versions, passing any value other than 'route' to next() is treated as an error. Express then skips ordinary routing middleware for that request.
Step 3: Log centrally, respond safely
Error middleware is identified by its four parameters, (err, req, res, next). It must be registered after the routes and middleware whose errors it handles (see the middleware guide).
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #3
app.use((err, req, res, next) => {
const requestId = requestContext.getStore()?.requestId;
console.error({
requestId,
method: req.method,
path: req.originalUrl,
error: err,
stack: err?.stack,
});
if (res.headersSent) {
return next(err);
}
res.status(err.statusCode || err.status || 500).json({
error: 'Internal Server Error',
requestId,
});
});
What this does and doesn’t establish
- The Express sources establish the mechanics: the four-argument signature and the
res.headersSentrule. They don’t establish a logging schema. - When headers have already been sent, Express documents delegating with
next(err)rather than writing a second response. Its default handler then closes the connection. - The status line is a teaching pattern. Classify expected client errors (validation, not found) separately from unexpected failures, and don’t blindly pass through arbitrary
err.statusvalues from libraries. - Be selective about request fields. Never log secrets, authorization headers, or sensitive bodies. In real deployments, replace
console.errorwith a structured logger that emits the ID as a field.
Because the ID is returned in the response body, a client can quote it in a support request and you can find the matching server-side record.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Should I send the stack trace to the API client?
No, not in production. Express’s built-in handler uses a valid status or statusCode from the error and otherwise 500. In production it returns only a status message, while outside production it includes the stack (Express 5.x guide). The errorhandler middleware is meant for development only, and it warns that it exposes full stacks and internal details. Keep stacks and internal objects in server logs, and give clients a generic message plus the request ID.
Quick Recap
Rank #4
Preserving the original exception and its stack
- Log the Error object itself, not just
err.message. A stack describes where the Error was instantiated. It is built through V8’s stack-trace API and limited byError.stackTraceLimitor the available frames. - Wrap with
cause. When adding domain context, usenew Error('Could not load order', { cause: err })so the original failure survives. Node’s v22.18.0 Errors documentation describeserror.causeand chained errors. Confirm your runtime supports the option, and make sure your logger actually serializescause. - Don’t expect the stack to supply context. It shows where the error was created. The request ID, method and route connect it to a request, and those come from the store and from
req.
Troubleshooting
- Request ID is
undefinedin the handler or logs. The context middleware may be registered after the route, or the log call may run outside anyrun()callback. Register it first, and confirmrun()wrapsnext(). - Error handler never runs on Express 4. An async rejection wasn’t forwarded. Add
try/catchwithnext(err), or.catch(next). - Error handler never runs on Express 5. A Promise was probably started but not returned, or the failure happened in a timer or callback with no forwarding.
- Error handler is skipped for some routes. It is registered before those routes. Move it after all routes and middleware.
- “Cannot set headers after they are sent.” Check
res.headersSentand delegate withnext(err).
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




