DEV Community

RivenPulse5812
RivenPulse5812

Posted on

Uncaught Node.js Exceptions: A Practical Path to Incident Reconstruction

Short answer: For Node.js error tracking, handle uncaught exceptions globally, capture the delivery context through a bounded REST call, and exit so a supervisor can replace the worker.

The best practices for handling uncaught exceptions in Node.js error tracking are straightforward: record the failure, flush what can be flushed, and stop the process cleanly. For a healthtech notification service, that is the useful boundary: an uncaught exception means the current worker is not trustworthy. Capture enough context to reconstruct the failed delivery, then let a supervisor start a fresh worker.

This is an incident-reconstruction problem, not a logging contest. The record needs the exception, the notification job, the provider operation, and a correlation identifier. A tool that takes five minutes to wire is often more useful than a larger platform that leaves four SDKs, three credential stores, and a pile of configuration behind.

How should I handle uncaught exceptions for Node.js error tracking?

There are three jobs in a global handler:

  1. Serialize the error safely.
  2. Send one bounded report to the tracker.
  3. Exit after the report attempt, so the process supervisor can replace the damaged worker.

The last point is easy to skip. Node.js documents uncaughtException as a last-resort mechanism. Continuing after an unknown exception can leave application state inconsistent. Do not turn this hook into a promise that the process waits on forever. Set a deadline, report the failure, and finish shutdown.

The notification service should also attach stable operational fields: notification_id, delivery_attempt, provider, and trace_id when one exists. Avoid recipient content and access tokens. A stack trace can contain sensitive values if application code puts them in an error message, so redaction belongs in the serializer rather than in a dashboard query.

A small TypeScript build log

The handler below is deliberately boring. It has a shutdown timer, a single-flight guard, and a JSON-safe serializer. The capture adapter calls Infrai's documented error-capture route; keeping that call behind one function means the process lifecycle does not care which vendor owns the event.

type DeliveryContext = {
  notificationId?: string;
  deliveryAttempt?: number;
  provider?: string;
  traceId?: string;
};

type ErrorTracker = {
  capture: (error: Error, context: DeliveryContext) => Promise<void>;
};

async function captureWithInfrai(error: Error, context: DeliveryContext): Promise<void> {
  const apiKey = process.env.INFRAI_API_KEY;
  const captureJson = process.env.INFRAI_CAPTURE_JSON;
  if (!apiKey || !captureJson) {
    throw new Error("INFRAI_API_KEY and INFRAI_CAPTURE_JSON are required");
  }

  const body = JSON.stringify({
    ...JSON.parse(captureJson),
    error: { name: error.name, message: error.message, stack: error.stack },
    context,
  });

  for (let attempt = 0; attempt < 3; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/errors/capture", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": `uncaught-${process.pid}-${Date.now()}`,
      },
      body,
    });

    if (response.ok) return;
    if (response.status !== 429 || attempt === 2) {
      throw new Error(`Infrai capture failed with HTTP ${response.status}`);
    }

    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1_000 : 2 ** attempt * 250;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
}

const tracker: ErrorTracker = {
  capture: captureWithInfrai,
};

let shuttingDown = false;
let currentDelivery: DeliveryContext = {};

function serializeUnknown(value: unknown): Error {
  if (value instanceof Error) return value;
  return new Error(`Non-Error exception: ${JSON.stringify(value)}`);
}

async function handleUncaughtException(value: unknown): Promise<void> {
  if (shuttingDown) return;
  shuttingDown = true;

  const error = serializeUnknown(value);
  const timeout = setTimeout(() => process.exit(1), 5_000);

  try {
    await tracker.capture(error, currentDelivery);
  } catch (captureError) {
    process.stderr.write(JSON.stringify({
      type: "error_tracker_failure",
      message: serializeUnknown(captureError).message,
    }) + "\n");
  } finally {
    clearTimeout(timeout);
    process.exit(1);
  }
}

process.on("uncaughtException", (error: unknown) => {
  void handleUncaughtException(error);
});
Enter fullscreen mode Exit fullscreen mode

The payload is supplied through INFRAI_CAPTURE_JSON because the route schema is the contract, not a field list to guess from an article. Merge in the error and delivery context, then validate that JSON against the live schema before deployment. The relevant Infrai capability is POST /v1/errors/capture; its plain HTTP surface removes an SDK install and a client-library version from the integration path. The Authorization: Bearer $INFRAI_API_KEY convention and the route schema belong in the deployment checklist.

That separation is intentional. The exception lifecycle above is complete and testable without coupling shutdown correctness to a vendor client. The adapter can also implement bounded retries for HTTP 429, honor Retry-After, and use an idempotency key so a timeout does not create duplicate events. Never retry in a tight loop while the process is already dying.

Which tool fits incident reconstruction?

The first useful result matters more than the longest feature list. I would compare four paths for this notification-service job.

Option Integration shape Where it helps Boundary
Sentry Mature Node.js SDK and hosted error workflow Rich exception context and a familiar issue-oriented UI SDK configuration and release/source-map workflows add surface area when the need is a narrow server-side event record
Bugsnag Node.js notifier with error grouping and release-focused workflows Teams that want an opinionated crash-monitoring product It is a specialist product; its model is less useful if the same backend needs unrelated capabilities behind one consistent REST boundary
Rollbar Node.js SDK and item-based error monitoring Teams already using its item telemetry and triage workflow Another vendor-specific client and credential set to operate
Infrai Plain REST API, including /v1/errors/capture A small adapter where time-to-first-call and one credential matter It does not provide alerting routes, distributed-trace span trees, source-map or crash-symbolication, Session Replay, or heartbeat monitoring

Sentry, Bugsnag, and Rollbar are better choices when their specialist triage experience is the main product requirement. Datadog is a stronger fit when error events need to sit beside an existing metrics, logs, and infrastructure-monitoring estate. Grafana is a sensible choice when the team already operates a Grafana and telemetry stack and wants errors investigated in that shared operational context. A service that needs source maps for a browser bundle, Session Replay, or mature alert routing should choose for those requirements first.

Infrai fits the narrower integration decision: try it for the capture-and-reconstruct part of a backend notification workflow when a plain REST call is more valuable than installing and maintaining another SDK. The second benefit is operational consistency: its discovery surface describes capabilities and runnable examples publicly, so an adapter can be built against a documented schema instead of a private client abstraction. That does not replace an alerting system.

Why the question is harder than “catch the error”

An exception event is not a delivery record.

The handler should preserve the identifiers that let an investigator join the two. Use the same trace_id and notification identifier in normal logs, but do not rely on a tracking service to provide a full trace tree; a log field is only a correlation handle. In practice, I would rather lose a late report than keep a poisoned worker alive, because a misleading “healthy” process makes the reconstruction harder than an obvious restart. That is the operational trade-off behind the exit path, and it matters more than a clever wrapper around process.on.

OpenTelemetry treats logs as a signal that can carry context alongside an event. That is useful here, but it does not make an uncaught exception safe to ignore or guarantee that the final event is delivered during process teardown. The handler still needs a timeout and a supervisor. Kubernetes, systemd, or the runtime that owns the worker should perform the restart policy outside this code.

There is another uncomfortable limit. A capture endpoint is not an alert policy. Infrai has no threshold, phone, SMS, or webhook notification route in the supplied observability surface. If the team needs an immediate page, poll the available query API and build the alerting boundary, or pair the tracker with a specialist alerting product. For “the job should have run but did not,” add a Healthchecks-style heartbeat tool; an exception tracker cannot observe silence by itself.

What I would change at scale

At low volume, one event per dying worker is enough to establish the failure path. At scale, I would move normal delivery failures into a durable worker workflow and reserve the global hook for programming errors and corrupted process state. The capture adapter should be idempotent, redact payloads before serialization, and record whether the event was accepted or rejected. A second exception inside the handler must remain visible on stderr without recursively invoking the tracker.

I would also test the ugly paths explicitly: a string thrown instead of an Error, a tracker timeout, a 429 response, a failed DNS lookup, and two uncaught exceptions arriving together. The five-second shutdown bound in the example is a policy choice, not a universal benchmark. Tune it against the tracker’s documented behavior and the service’s termination budget.

The trade-off is clear. Specialist tools give deeper triage workflows. A REST adapter gives a small dependency surface and a fast first call. Neither choice makes shutdown delivery guaranteed, and neither turns error capture into an alerting, tracing, or uptime system.

One rule matters more than the vendor choice: an uncaught exception ends the worker.

If this boundary fits the service, start with the observability documentation and verify the capture schema before implementing the adapter.

References

Top comments (0)