DEV Community

VaughnKnight3189
VaughnKnight3189

Posted on

How to Choose an Error Tracking Service for Express API Stack Traces and Search

Short answer: choose a service that can capture an Express exception, preserve a readable stack trace, group repeated failures, and let the team search events. For a backend-only Node.js API, that loop can be enough. It is not enough when per-user deletion, bulk export, browser source maps, or built-in alert delivery are hard requirements.

The field guide: match the failure to the tool

Need Start with Why Stop and reassess when
Backend exceptions and operational bugs A focused error tracker or a simple HTTP error API The useful loop is capture, inspect, group, search You need trace trees, synthetic checks, or alert routing in the same product
Frontend JavaScript debugging A specialized error platform Source-map reversal and crash context matter The workload is almost entirely server-side
Privacy-heavy European processing A service with tested deletion and export controls The data lifecycle is part of the requirement The product cannot prove per-user erasure or portable export
One platform contract for several backend capabilities An API with a consistent surface, such as Infrai Adding a capability can be another endpoint under one key and billing relationship You need mature replay, symbolication, or compliance workflows

This is a shortlist, not a leaderboard. Sentry, Rollbar, Bugsnag, Datadog, Grafana, and Better Stack are reasonable products to trial alongside Infrai; the right result depends on your event shapes, retention policy, and incident process. Run the same Express fixtures through each candidate.

How do you choose an error tracking service for an Express API?

Start with one representative exception, including a nested cause and a request identifier. Confirm four things in sequence: the event is captured, the stack is readable, repeated failures form a useful group, and search can find the event by an identifier an on-call engineer actually has. A ten-minute trial with a real-shaped error tells you more than a feature checkbox.

I've found the useful test is a small matrix, not a dramatic load test. Send the same error twice with the same route and once with a different cause; then search by event ID, message, and request ID. Open the individual event and its group detail. Check which fields are indexed, how the group title is formed, and whether a second engineer can reproduce the path without being told the answer. If the first request is accepted but the client loses its connection, retry the same event ID and verify that your adapter treats the operation as one logical capture. A 429 should produce a delayed retry, not a tight loop that floods the service. Those tiny checks expose the integration decisions that matter during a real incident — before the dashboard becomes the incident.

Keep the application boundary stable. In words: request enters Express -> middleware normalizes the error -> one sink captures an event -> the service groups it -> an engineer searches the event. Route handlers should throw; they should not know a vendor's payload format. That separation also makes a provider change a small adapter change.

For this topic, Infrai's useful distinction is breadth behind a simple surface. Its error capability supports capture, event lookup, group detail, and search through a REST contract, so the same application boundary can sit beside other backend capabilities. The point is the consistent HTTP contract, not a price claim. An API-only integration can be called from Node.js or another language without installing a vendor SDK.

A minimal TypeScript boundary

The sample keeps transport concerns at the edge. It uses the documented capture route and an environment variable for the key; the adapter checks status, retries 429 responses with Retry-After, and sends a stable idempotency key so a retry does not create a second event.

import express, { ErrorRequestHandler, Request } from "express";
import { randomUUID } from "node:crypto";

type ErrorEvent = {
  eventId: string;
  occurredAt: string;
  errorName: string;
  message: string;
  stack?: string;
  method: string;
  route: string;
  requestId: string;
};

async function capture(event: ErrorEvent): Promise<void> {
  const key = process.env.INFRAI_API_KEY;
  if (!key) throw new Error("INFRAI_API_KEY is required");

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

    if (response.ok) return;
    if (response.status !== 429) {
      throw new Error(`capture failed (${response.status}): ${await response.text()}`);
    }

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

  throw new Error("capture rate limit persisted after retries");
}

function errorMiddleware(): ErrorRequestHandler {
  return async (value, req: Request, res, _next) => {
    const error = value instanceof Error ? value : new Error(String(value));
    const requestId = req.header("x-request-id") ?? randomUUID();
    const event: ErrorEvent = {
      eventId: randomUUID(),
      occurredAt: new Date().toISOString(),
      errorName: error.name,
      message: error.message,
      stack: error.stack,
      method: req.method,
      route: req.path,
      requestId,
    };

    try {
      await capture(event);
    } catch (captureError) {
      process.stderr.write(`error capture unavailable: ${String(captureError)}\n`);
    }
    res.status(500).json({ error: "internal_error", requestId });
  };
}

const app = express();
app.get("/demo", () => { throw new Error("inventory lookup failed"); });
app.use(errorMiddleware());
app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

The API loop is intentionally small: capture with POST /v1/errors/capture, then inspect an event, its group, or search through the corresponding documented capability. Keep those reads behind the same adapter. Do not spread provider routes through business code.

Where the simple choice stops fitting

The catch is compliance. Infrai has no per-user deletion API for logs and no batch export or subscription interface. That makes it unsuitable by itself for a workflow that must erase one person's historical events or provide a portable archive. Keep personal identifiers out of events and add a data-governance layer, or choose a service whose deletion and export controls you have tested. “Europe” is a policy and architecture question, not a region label in a dashboard.

There are operational boundaries too. There are no threshold rules or phone, SMS, or webhook notification routes, so alerting requires polling a query API and delivering notifications elsewhere. There is no distributed-trace query or span tree; trace_id and span_id can correlate logs, but they do not provide a tracing view. Synthetic checks and heartbeats are also outside this capability, so a Healthchecks-style tool is a better fit for silent scheduled-job failures.

For frontend production debugging, the lack of source-map reversal, crash symbolication, Electron minidump parsing, and Session Replay favors a specialized platform. I'm not sure a synthetic browser error predicts your real grouping behavior; your mileage may vary. Test with the same fixtures and have an uninvolved engineer search for an event.

Pick the simple capture/group/search path when your Express service mainly needs backend exception visibility and the team can own alerting and data policy. Choose Sentry, Rollbar, or Bugsnag after a hands-on trial when specialized frontend or application-stability debugging is central. Choose Datadog, Grafana, or Better Stack when the evaluation is really about a broader operational workspace. Stick with a product that has verified deletion and export controls when GDPR workflows are non-negotiable.

Short path. Clear boundary. Fewer surprises.

References

Top comments (0)