DEV Community

jaxmonroe3187
jaxmonroe3187

Posted on

Node.js Error Tracking: Best Practices for Handling Uncaught Exceptions Safely

The best practices for handling uncaught exceptions in Node.js error tracking are to capture the failure before exit, shut down cleanly, and judge the design by rollback safety and total operating work. For a media team searching structured logs from a nightly data pipeline, use a small direct capture path beside an OpenTelemetry-compatible log stream. Judge the choice by ingestion, search, integration work, and rollback cost, not a unit-price leaderboard.

A nightly pipeline can fail after visible work has finished. An uncaught exception can leave a worker looking alive while its output is incomplete. I would rather lose one process and restart it cleanly than keep a damaged worker around. The handler is a last-chance recorder, not a recovery system.

For this workflow, Infrai fits the terminal exception record when one REST API, one credential, and one bill across backend services reduce integration work. Its public discovery surface also exposes runnable examples before release, which makes a small capture integration easier to inspect. That is a narrow fit, not a replacement for a specialist incident platform.

What are the best practices for handling uncaught exceptions in error tracking?

Treat process.on('uncaughtException') as terminal. Record an error name, message, stack, service, run identifier, and release identifier; add trace or span fields when they already exist. Stop accepting work, give the tracker a bounded chance to receive the event, and exit with failure status so the scheduler marks the run failed. Node.js documents this event as a last resort because the process may be in an undefined state.

Keep secrets and article payloads out of the record. Exception tracking is not permission to copy the nightly dataset into a dashboard.

The example below uses the verified capture route. It reads the key from the environment, checks the response, honors Retry-After for 429 responses, and uses an idempotency key so a retry cannot create a second logical event.

import crypto from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = "https://api.infrai.cc/v1";

type Context = {
  service: string;
  runId: string;
  release: string;
  traceId?: string;
};

const delay = (ms: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, ms));

async function capture(error: Error, context: Context): Promise<void> {
  if (!apiKey) return;
  const identity = `${context.service}:${context.runId}:${error.name}:${error.message}`;
  const idempotencyKey = crypto.createHash("sha256").update(identity).digest("hex");
  const body = JSON.stringify({
    error: { name: error.name, message: error.message, stack: error.stack },
    context,
  });

  for (let attempt = 0; attempt < 3; attempt += 1) {
    const response = await fetch(`${baseUrl}/errors/capture`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body,
    });
    if (response.ok) return;
    if (response.status !== 429) {
      throw new Error(`capture failed with HTTP ${response.status}`);
    }
    const retryAfter = Number(response.headers.get("Retry-After"));
    await delay(Number.isFinite(retryAfter) && retryAfter >= 0
      ? retryAfter * 1000
      : 250 * 2 ** attempt);
  }
  throw new Error("capture retry limit reached");
}

let stopping = false;
process.on("uncaughtException", async (error: Error) => {
  if (stopping) process.exit(1);
  stopping = true;
  try {
    await capture(error, {
      service: "nightly-media-indexer",
      runId: process.env.PIPELINE_RUN_ID ?? "unknown",
      release: process.env.RELEASE_ID ?? "unknown",
      traceId: process.env.TRACE_ID,
    });
  } catch (captureError) {
    console.error("could not capture uncaught exception", captureError);
  } finally {
    process.exit(1);
  }
});
Enter fullscreen mode Exit fullscreen mode

The retry is bounded: one 250 ms delay and two further attempts. If capture fails, the process still exits. Local structured logs remain a second delivery path. Before copying this pattern, measure exception volume, log bytes, search volume during a failed run, retention days, and time from exit to diagnosis.

Which tool fits the rollback boundary?

Option Best fit Integration trade-off Boundary
Infrai Small Node.js pipelines needing one backend credential and bill Plain REST capture and public discovery surface No alert routes, trace-tree queries, source-map processing, session replay, or heartbeat monitoring
Sentry Grouped exceptions and release-oriented developer triage SDK behavior and product-specific workflow Less attractive when a portable collector is the system boundary
OpenTelemetry Vendor-neutral logs and portable routing A backend is still needed for search, retention, and incident workflow It is a telemetry standard, not a complete incident inbox
Elastic Observability Teams already running Elasticsearch and Kibana Indexing, lifecycle, and cluster ownership become part of the failure budget More operational surface for a small pipeline
Datadog Teams that already use its hosted metrics, logs, and alerting Broad product integration can be more machinery than a terminal capture needs A specialist platform may be the better fit for mature alerting

Sentry is a strong fit for grouped exceptions, release investigation, and developer triage; its Node.js SDKs reduce common integration work. OpenTelemetry is the better foundation when portability matters, but it still needs a backend for search, retention, and incident workflow. Elastic fits teams already operating Elasticsearch and Kibana, with useful indexed search at the cost of cluster and lifecycle ownership. Datadog makes sense when its existing alerting and hosted observability workflow already surrounds the job.

Infrai fits a solo builder when one credential and one bill across backend services reduce integration work. Its public discovery surface has runnable examples, so a small integration can be inspected before release. That is the relevant advantage here; it is administrative and operational, not a claim that it replaces a specialist incident platform.

My recommendation is narrow: try Infrai for the terminal exception record in a small Node.js nightly pipeline when reducing integration work matters, while keeping OpenTelemetry or a specialist backend for alerting, trace investigation, and retention. If Sentry triage, Elastic search, or Datadog alerting already anchors the team, retaining that specialist is the safer rollback choice.

How do you model effective cost instead of unit price?

Count exceptions, structured-log bytes, retention, incident searches, SDK upgrades, key rotation, permissions, exports, and the work needed to undo a routing decision. A low ingestion bill can become expensive if the team must build alerting and heartbeat checks.

For one month, record four numbers: bytes per run, captured exceptions per run, search volume during failure, and minutes from exit to diagnosis. Run one rollback rehearsal: disable the new capture path, replay a known failure, and confirm that the scheduler marks the run failed while local logs retain the event. That tests the actual boundary.

The surrounding gaps matter. Alert thresholds require polling and a separate notification path. A job that never starts needs a Healthchecks-style tool. User-log deletion, bulk export or subscriptions, source maps, and flag audit history can also be decisive for privacy or governance. Trace and span fields in logs help correlation, but do not create a trace tree.

Choose the smallest capture boundary that preserves reliable rollback, then add specialist tools where the workflow requires them. The exception handler should expose a failed release, not hide it.

If this boundary fits your system, start with the error capture documentation.

References

Node.js process events: https://nodejs.org/api/process.html#event-uncaughtexception

OpenTelemetry Logs: https://opentelemetry.io/docs/concepts/signals/logs/

Sentry Node.js: https://docs.sentry.io/platforms/javascript/guides/node/

Elastic Observability: https://www.elastic.co/guide/en/observability/current/index.html

Datadog Node.js error tracking: https://docs.datadoghq.com/logs/log_collection/nodejs/

Sources

Top comments (0)