Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan Now×
Skip to content
World desk5 min

How to Capture Node.js Express API Errors With Request Context and Stack Traces

A version-aware pattern for logging Express errors with a request ID and full stack on the server, while keeping API responses safe.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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() over enterWith(). Node’s docs favor run(), because enterWith() can persist across later synchronous event-handler work.
  • Handle a missing store. getStore() returns undefined outside a context started with run() or enterWith(). 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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:

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.headersSent rule. 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.status values from libraries.
  • Be selective about request fields. Never log secrets, authorization headers, or sensitive bodies. In real deployments, replace console.error with 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.Support on Ko-Fi

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.

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 by Error.stackTraceLimit or the available frames.
  • Wrap with cause. When adding domain context, use new Error('Could not load order', { cause: err }) so the original failure survives. Node’s v22.18.0 Errors documentation describes error.cause and chained errors. Confirm your runtime supports the option, and make sure your logger actually serializes cause.
  • 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 undefined in the handler or logs. The context middleware may be registered after the route, or the log call may run outside any run() callback. Register it first, and confirm run() wraps next().
  • Error handler never runs on Express 4. An async rejection wasn’t forwarded. Add try/catch with next(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.headersSent and delegate with next(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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.