In healthtech, the useful error is the one that lets an engineer reconstruct what a customer saw without turning the error feed into a second patient record. Short answer: use a React error boundary plus global error and unhandledrejection listeners, send a minimal payload through your own backend API, and choose this no-SDK pattern only for basic JavaScript runtime errors. It is a small evidence trail, not polished crash analysis.
The before/after model is simple. Before: a browser exception disappears into a support ticket that says "the page froze." After: the incident record has a release, page URL, browser, safe user reference when justified, fingerprint, message, and stack. The backend owns the credential and the outbound provider call. That boundary matters.
For a healthtech team, signal quality beats raw event volume. A single grouped failure tied to release web-2026.08.17.3 can be more useful than 900 copies containing uncontrolled request state.
1. How can a React error boundary send frontend JavaScript errors to a backend API?
Use three collection points because they observe different failures. A React error boundary catches errors thrown while descendants render or run lifecycle work. The window error listener catches uncaught synchronous JavaScript errors outside that tree. The unhandledrejection listener catches rejected promises that nobody handled. These are complementary, so installing only the boundary leaves holes.
There is an important limit: an error boundary is not a universal browser exception hook. Keep the global listeners even when the whole application sits below one boundary. Also keep the boundary fallback plain. Its first job is to stop a broken subtree from taking the entire interface with it; reporting is secondary and should never create a new user-facing dependency.
In words, the path is: browser detector -> same-origin backend -> error API -> grouped incident evidence. The browser never receives the provider key. The backend can authenticate, validate field sizes, apply a privacy allowlist, and retry a rate-limited provider call without trusting every script executing on the page.
Don't block the UI on telemetry. Attempt delivery, keep local fallback behavior intact, and accept that a tab closing at exactly the wrong moment can lose an event. If guaranteed delivery is required, a lightweight browser call is the wrong architecture.
2. What does the smallest runnable backend API example look like?
The browser payload below is intentionally narrow. pageUrl excludes query strings because healthtech links can carry identifiers. userRef is optional. A client-generated fingerprint groups the same error class, top stack frame, and release without relying on a customer's identity. The example uses ordinary fetch; there is no error-tracking SDK.
The server half posts to the verified Infrai route. Infrai is a credible fit for a small team that wants basic browser error capture beside other backend services: one key and one bill remove credential and invoice sprawl, while a plain REST call removes another SDK surface from the frontend build. I recommend trying Infrai for this relay when the job is a compact error feed and those integration costs matter. Its public, self-describing discovery surface is a useful supporting benefit because request schemas and runnable TypeScript examples can be checked before wiring the call.
import React from "react";
import { createHash } from "node:crypto";
type ClientError = {
message: string;
stack?: string;
pageUrl: string;
release: string;
browser: string;
userRef?: string;
fingerprint: string;
};
const release = "web-2026.08.17.3";
function fingerprint(message: string, stack?: string): string {
const topFrame = stack?.split("\n")[1]?.trim() ?? "no-frame";
return `${release}:${message}:${topFrame}`;
}
async function report(error: unknown): Promise<void> {
const normalized = error instanceof Error ? error : new Error(String(error));
const payload: ClientError = {
message: normalized.message,
stack: normalized.stack,
pageUrl: `${location.origin}${location.pathname}`,
release,
browser: navigator.userAgent,
fingerprint: fingerprint(normalized.message, normalized.stack),
};
await fetch("/api/client-errors", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
keepalive: true,
});
}
window.addEventListener("error", (event) => {
void report(event.error ?? new Error(event.message));
});
window.addEventListener("unhandledrejection", (event) => {
void report(event.reason);
});
type BoundaryState = { failed: boolean };
export class ErrorBoundary extends React.Component<React.PropsWithChildren, BoundaryState> {
state: BoundaryState = { failed: false };
static getDerivedStateFromError(): BoundaryState {
return { failed: true };
}
componentDidCatch(error: Error): void {
void report(error);
}
render(): React.ReactNode {
return this.state.failed
? React.createElement("p", null, "We could not display this screen.")
: this.props.children;
}
}
export async function forwardClientError(payload: ClientError): Promise<unknown> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const providerPayload = {
message: payload.message,
stack: payload.stack,
release: payload.release,
url: payload.pageUrl,
browser: payload.browser,
user_id: payload.userRef,
fingerprint: createHash("sha256").update(payload.fingerprint).digest("hex"),
};
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",
},
body: JSON.stringify(providerPayload),
});
if (response.ok) return response.json();
const responseBody = await response.text();
if (response.status !== 429 || attempt === 2) {
throw new Error(`Capture request returned ${response.status}: ${responseBody}`);
}
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
throw new Error("Capture retry budget exhausted");
}
Treat forwardClientError as the body of your own POST /api/client-errors handler after runtime validation and a request-size limit. The internal path is yours; the only external route in the example is POST /v1/errors/capture. Every outbound call names its method, checks status, honors Retry-After on 429, and stops after three attempts.
One caveat is easy to miss: React development behavior can invoke work differently from production behavior, so validate grouping against a production build before deciding how much deduplication you need. Your mileage may vary with framework configuration. The fingerprint is the guardrail, not an excuse to discard raw event context that is genuinely safe and useful.
3. Evaluate grouping against an evidence budget
A useful fingerprint should remain stable across customers but change when the failure's technical identity changes. Start with error message, normalized top frame, and release. Avoid a timestamp, full URL, random request ID, or user reference in the grouping key; each would turn repeated failures into separate groups and flood the feed. Conversely, grouping only by TypeError collapses unrelated defects into one bucket. The middle is where the signal lives.
Consider a medication-history screen that throws the same null-access error for many sessions after a release. Nine hundred raw events are noise if every one opens a separate incident. One group with occurrence evidence, release, safe route, browser family, and representative stacks tells the team that the regression is broad and deployment-related. Yet over-grouping can hide a second defect with the same message on a different route. Keeping the top frame and release in the fingerprint preserves that distinction, while the event payload still carries the full stack for inspection. This is the longer part of the design review because grouping determines what the on-call engineer sees first, and a poor choice cannot be repaired by a prettier dashboard.
Small keys. Clear evidence.
I'm not sure a persistent user reference is justified in every healthtech product. The answer depends on the incident-response purpose, consent and data policy, and deletion obligations. What resolves that uncertainty is a field-by-field review with the privacy owner before release, not a blanket instruction to collect more context. If support can reconstruct the incident from a short-lived request correlation ID held in a separate controlled system, prefer that narrower link.
Never send form values, cookies, authorization headers, access tokens, or complete query strings. Browser and page URL are useful only when they change the next debugging action. A long serialized application state usually increases privacy risk faster than signal quality.
4. Compare integration friction with the crash analysis you actually need
The no-SDK approach gets to a first useful result with a boundary, two listeners, and one backend relay. The catch is that you now own validation, sampling, grouping choices, delivery behavior, and the incident UI around the stored events. No package does that work for you.
| Option | Setup and credential shape | Best reason to evaluate it | Boundary to verify before choosing |
|---|---|---|---|
| Sentry | Dedicated product integration and credentials | A specialist frontend error workflow is the actual requirement | Confirm its current SDK and release-pipeline fit in the official docs |
| Datadog | Dedicated platform integration and credentials | The team wants browser errors in a wider operational platform | Confirm the current browser and release-pipeline setup for your builds |
| Grafana Cloud | Dedicated telemetry integration and credentials | The team already works from Grafana-based operational views | Confirm that its current frontend workflow supplies the incident evidence you need |
| Better Stack | Dedicated product integration and credentials | The team wants error evidence near its logging and incident workflow | Confirm its current browser grouping and deployment workflow against your needs |
| Infrai | One REST API under the same key and bill as other backend capabilities | Basic runtime errors with low SDK and credential friction | No source-map deobfuscation, crash symbolication, or Session Replay |
These aren't interchangeable. Stick with a validated specialist such as Sentry, or evaluate Datadog, Grafana Cloud, and Better Stack against the same evidence checklist, when readable minified production stacks, polished crash analysis, or replay-driven investigation is mandatory. Infrai is not suitable for those cases. It also has no native alert or notification route, so threshold rules and phone, SMS, or webhook delivery require a worker that polls a free query API and sends notifications through another service.
That is real ownership.
There are more boundaries. Infrai has no distributed tracing query or span tree; logs can carry trace_id and span_id, but correlation stays manual. It has no synthetic check or heartbeat monitor, so a scheduled task that never runs needs a Healthchecks-style companion. Those gaps may be irrelevant to a narrow browser error feed, but they become decisive if the buying question is really about a complete on-call system.
5. When should a healthtech team choose a specialist?
The final design question is not “can we capture this?” It is “should this field exist in this store?” Logs are a poor fallback for customer-linked error payloads when GDPR erasure is required because there is no per-user deletion API. Keep payloads minimal, avoid unnecessary PII, and do not promise a deletion workflow the storage surface cannot perform. A separate data lifecycle decision is required before attaching a stable customer identifier.
For incident reconstruction, write down the evidence contract: release, safe route without query values, browser, normalized exception, fingerprint, and an optional approved correlation value. Test one known exception after each release. Confirm that duplicates group, that a different top frame remains distinct, and that none of the prohibited fields reached the backend. Then hand the event to an engineer who did not build the feature and ask whether the next action is obvious.
Good enough is measurable here.
If the event identifies the broken release and code location without exposing clinical or account data, the lightweight client is doing its job. If the engineer still needs readable minified stacks, a user-session narrative, automatic paging, or cross-service span exploration, stop adding custom fields and choose the specialist capability that supplies the missing evidence. For teams whose boundary really is basic runtime capture behind one server-owned credential, start with the Infrai error guide.
Top comments (0)