How do I track errors in a Node.js app in production, and which tools should I compare?

Console logs are not enough: nobody reads them until a user complains, and they carry no grouping, alerts or release information. Load an error-tracking SDK before your application code, let it capture request errors and unhandled failures, flush pending events before the process exits, upload source maps for transpiled code, and tag every event with a release and an environment.

By the Bugsly team. Last updated . Markdown version

Why it happens

A Node.js server fails in places nobody is watching. A route handler throws, a promise rejects with nothing attached to it, a background job dies halfway through a queue. console.log and console.error write a line to a stream, and that is the end of it: the line is mixed with thousands of others, it has no count, no first-seen time, no link to the deploy that introduced it, and nobody is told. You find out when a customer writes in.

Process-level behaviour makes this worse. Per the Node.js documentation, an uncaught exception by default prints the stack trace to stderr and exits with code 1, and an unhandled promise rejection is treated as an uncaught exception under the default --unhandled-rejections mode (throw). So the worst failures also kill the process, which means anything buffered in memory, including an error report that has not been sent yet, can be lost unless you deliberately wait for it.

An error tracker solves three things logs do not: it groups repeated occurrences into one issue so you see counts instead of noise, it attaches context such as the request, breadcrumbs, release and environment, and it notifies someone when a new or returning problem appears. The steps below set that up in a way that works with any tracker that speaks the same SDK protocol, then compare the realistic approaches by criteria rather than by vendor.

How to track errors in a production Node.js app

  1. Decide what console.log cannot do for you, then pick an approach

    Write down what you need: grouping of duplicates, alerts on new errors, the release that caused it, stack traces that map to your source, and who gets notified. Plain logs cover none of these without extra tooling. The comparison table below lists four approaches against those criteria so you can choose before installing anything. For a tool-by-tool comparison, see the blog guide linked at the end.

  2. Initialise the SDK before any other code loads

    Sentry's Node.js guide tells you to create an instrument file that calls Sentry.init, then start your app with the --import flag so the file loads before the application starts (Sentry docs, read 2026-11-13). Loading it first lets the SDK instrument modules such as your HTTP framework before they are imported. Keep the DSN in an environment variable, not in source.

    // instrument.js
    import * as Sentry from "@sentry/node";
    
    Sentry.init({
      dsn: process.env.SENTRY_DSN,
      release: process.env.GIT_SHA,
      environment: process.env.NODE_ENV,
      tracesSampleRate: 0.1,
    });
    
    // start the app with:
    // node --import ./instrument.js app.js
  3. Let the framework integration capture request errors

    For Express, Sentry's documentation says you do not need a separate SDK: set up the SDK for your runtime and its expressIntegration captures errors from routes and middleware automatically. By default errors with a 3xx or 4xx status are skipped, and you can change that with a shouldHandleError callback (Sentry Express docs, read 2026-11-13). If you also capture errors yourself inside your own error-handling middleware, that guide says to set shouldHandleError to false to avoid duplicate events. Your own error middleware should still be registered after your routes so it receives their errors, and it must send a response.

    // after all routes
    app.use((err, req, res, next) => {
      req.log?.error({ err }, "request failed");
      if (res.headersSent) return next(err);
      res.status(500).json({ error: "Internal Server Error" });
    });
  4. Treat unhandled rejections and uncaught exceptions as last-resort signals

    The Node.js documentation calls an uncaughtException handler a crude mechanism meant only as a last resort, because after an unhandled exception the application is in an undefined state and resuming it can cause further unpredictable problems. The documentation also notes that installing a handler overrides the default exit, and that a process with such a handler exits with 0 unless you set process.exitCode. So if you add your own handler, report the error, then exit non-zero and let your process manager restart the service.

    process.on("uncaughtException", (err) => {
      Sentry.captureException(err);
      Sentry.flush(2000).finally(() => process.exit(1));
    });
  5. Flush on shutdown so the last error is not lost

    The SDK queues events and sends them asynchronously. Sentry documents flush(timeout) as draining the queue while keeping the client usable, and close(timeout) as flushing and then disabling the SDK, to be called only right before shutting down (Sentry Node.js docs, read 2026-11-13). Call one of them in your SIGTERM handler and in short-lived scripts and serverless handlers, with a timeout so a slow network cannot hang the exit.

    process.on("SIGTERM", async () => {
      server.close();
      await Sentry.close(2000);
      process.exit(0);
    });
  6. Add source maps if you ship transpiled code

    Plain JavaScript run as written has readable stack frames. If you run TypeScript compiled to a dist folder or a bundled build, frames point at generated files and need source maps uploaded for the same release your SDK reports. The step-by-step version is in the minified stack traces solution linked below.

  7. Tag every event with release and environment, then verify

    Set release to the commit SHA or version you deployed and environment to production, staging and so on, so you can see which deploy introduced an error and filter out staging noise. Then trigger one deliberate error in a staging deploy and confirm it arrives with the right release, a readable stack and the request context before you rely on it.

Four approaches to Node.js error tracking compared by criteria
CriterionConsole logs onlyProcess handlers + log aggregationHosted error trackerSelf-hosted error tracker
Groups duplicates into issuesNoOnly with custom queriesYesYes
Alerts on a new or returning errorNoIf you write and maintain alert rulesBuilt inBuilt in
Release and deploy contextOnly if you log it yourselfOnly if you log it yourselfYes, from release and environment tagsYes, from release and environment tags
Source map supportNoManualUsuallyDepends on the product
Setup effortNoneModerate: shipping, storage, parsingLow: install SDK, set DSNHighest: run and upgrade the server yourself
Where error data livesYour serversYour log storeVendor infrastructureYour infrastructure
Ongoing operations burdenNoneLog pipeline and retentionVendor runs itYou run it

Criteria table written from the failure modes described on this page; it makes no vendor claims. For named tools, use the comparison blog guide linked below.

When to use Bugsly

Bugsly is a hosted error tracker that accepts events from the standard Sentry Node.js SDK, so the steps above work as written: keep your instrument file and change the DSN to the one from your Bugsly project (https://<public_key>@bugsly.dev/<project_id>). There is also a thin wrapper, @bugsly/node. Events are grouped into issues by fingerprint, an error that comes back after you resolved it is flagged as a regression, and alerts can go to email, Slack, Telegram or a webhook.

Each issue gets an AI analysis (a likely root cause, an explanation and a suggested fix with a confidence number) built from the stack trace, breadcrumbs and context. It is a model output and can be wrong, so treat it as a hypothesis. Bugsly also exposes a Model Context Protocol server, which lets an assistant read your issues and events directly. See the Node.js integration page for the exact setup.

When Bugsly is not the right answer

If your requirement is that error data never leaves your own infrastructure, a hosted service is not the answer, and Bugsly cannot be self-hosted: it is closed-source SaaS. Use a self-hosted tracker, or the log-aggregation approach in the table, and accept the operating cost that comes with it. Bugsly also has no single sign-on and no native minidump ingestion, and it will not import your history from another tool; a new project starts fresh.

Ask your AI assistant to do it

With the Bugsly MCP server connected (https://bugsly.dev/api/mcp; setup for ChatGPT, Claude, Cursor and Gemini CLI is on /mcp), paste this. The assistant reads first and reports back; you decide what happens next.

Assign to Bugsly: use list_projects to find my Node.js project, then list_issues for it and tell me which unresolved issues were most recently seen. For the top one, call get_issue and get_issue_events and tell me whether the stack frames show my original files, which release and environment reported it, and whether it looks like an unhandled rejection or a request error. Do not resolve or ignore anything; show me what you found so I can approve the next step.

Frequently asked questions

Is console.error enough for production Node.js apps?

No. It writes a line with no grouping, no alert and no release context, so you learn about failures from users. Use it for local debugging and pair it with an error tracker for production.

Why must the SDK load before my application code?

Sentry's Node.js guide starts the app with node --import ./instrument.js so the SDK initialises before your modules load and can instrument them. Initialising later can miss early errors and instrumentation.

Should I handle uncaughtException myself?

Only as a last resort. Node.js documents that the process is in an undefined state after an uncaught exception, so report the error, flush, and exit non-zero rather than carrying on.

Do I need to flush the SDK before the process exits?

Yes for short-lived processes, scripts, serverless handlers and shutdown hooks. Events are sent asynchronously, so call flush or close with a timeout before exiting.

Sources

Related

Sentry is a trademark of its owner; Bugsly is not affiliated with or endorsed by Sentry. “Sentry-compatible” only means Bugsly accepts events sent by Sentry's open SDKs.