Skip to content
Everframe Docs
Documentation

Crash reporting

Not every bug has a user willing to file it. Uncaught errors become reports on their own.

Updated

Crash reporting is on by default — the payload is data the SDK already holds (the exception plus the breadcrumb trail), fully redacted. It is a client veto:

<EverframeProvider config={{ sdkKey: 'evf_live_…', crashReporting: { disabled: true } }}>

Crash reporting also covers what happens below the bridge: JVM crashes on Android, native crashes, ANRs and low-memory kills on Android 11 and later, and native crashes on iOS and tvOS. Errors no error boundary catches while rendering are reported too. disabled: true turns all of them off. See Android crash reporting and iOS crash reporting.

What is caught

mechanismFires on
errorutilsAn uncaught JS error, via React Native’s ErrorUtils handler. Both platforms.
errorutils, from React’s onUncaughtErrorA component that throws while rendering and no error boundary catches, reported as a fatal JS crash with React’s component stack.
uncaught-exception-handlerA JVM crash below the bridge — Android only. Native crashes, ANRs and foreground low-memory kills on Android 11+ arrive as their own reports on the next launch.
native-mach, native-signal, native-nsexceptionA native crash on iOS or tvOS, recorded by the iOS SDK and sent on the next launch. See iOS crash reporting.

A fatal JavaScript error that was already stored is not reported again as a native abort.

Unhandled promise rejections are opt-in, with crashReporting: { promiseRejections: { enabled: true } }. A rejection still unhandled 2 seconds later becomes a non-fatal report with mechanism unhandledrejection. Only qualified runtimes are observed: react-native-tvos 0.85.3-0 with its Hermes in a Release build, on Android and iOS phones and tablets. Other React Native versions, Debug builds, JSC and TV devices are reported as unsupported by getPromiseRejectionStatus(), and their rejections do not become reports. Details are in the SDK README.

What happens next depends on whether the process survives. A non-fatal error leaves your app running, so the report uploads immediately. A fatal one — a native crash, or a JS error React Native treats as fatal — kills the process, so the report is written to the outbox and sent on the next launch.

Non-fatal errors are throttled per session, so a render loop does not flood you with reports. Fatal errors always report.

Report a caught exception

Use the hook inside a component, or import captureException from @everframe/react-native outside React:

import { useEverframe } from '@everframe/react-native';

const { captureException } = useEverframe();

try {
  await saveCart();
} catch (error) {
  captureException(error, {
    severity: 'warning',
    context: 'checkout.save-cart',
    metadata: { attempt: 2 },
  });
}

The signature is captureException(error: unknown, options?: CaptureExceptionOptions): void. It records handled: true, fatal: false, and mechanism captureException without opening the reporter. Options use the shared info / warning / error severity, short context, and bounded redacted metadata contract. From your own error boundary, pass { componentStack: info.componentStack } as well: it goes under the message, as for errors no boundary catches, so the error page names the components. Capture is best effort and returning does not confirm storage or delivery.

This API spans JavaScript, the generated bridge, and the native SDK, so rebuild the host app when adopting it: a JS-only update cannot add a native method.

occurredAt vs submittedAt

They differ, and for native crashes they can differ by a lot. Order your triage on occurredAt, not submittedAt, or a crash loop looks like it all happened at once when the user finally reopened the app.

What arrives

{
  "exceptionType": "TypeError",
  "message": "cart.lines is undefined",
  "frames": [
    { "raw": "at Cart (cart.tsx:42:11)" }
  ],
  "mechanism": "errorutils",
  "handled": false,
  "occurredAt": "2026-06-15T11:59:58.400Z",
  "fingerprint": "9f2c1ab30d84e177"
}

The envelope keeps each Raw stack line. For Hermes releases, configure the exact jsBundle identity and upload the final bytecode plus final composed map through the private source-map workflow. When processing succeeds, the error detail opens in Mapped mode; Raw remains available. Intermediate Metro maps cannot translate final Hermes bytecode offsets.

mechanism is a plain string, not an enum, so v2 can add values without older receivers rejecting reports.

Grouping

fingerprint is 16 hex characters computed on the client from the exception type and the top frames — a heuristic, deliberately, so grouping works with no server round-trip and no symbolication step. Treat it as a strong hint, not an identity.

Native crashes, on Android and on Apple platforms, are grouped by the mapped function once their symbols are uploaded, so the same crash in a new build joins its existing issue; one whose top frames have no mapped line keeps an issue per build for now. JavaScript errors on Android are grouped the same way once their Hermes source map is uploaded; on iOS they keep the client fingerprint. There is nothing to configure. Every symbol upload runs through @everframe/cli; see Symbols & source maps.

What comes with it

An unattended report’s title is generated from the exception — TypeError: cart.lines is undefined — and its description is empty, since nobody typed one. What makes it useful is everything else: the breadcrumb trail leading to the throw, the console state, the screen the user was on, and the device and app context. There is no screenshot and no replay — the process may already be gone, and the payload is built from the buffers only.

The JS trail survives a fatal error because breadcrumbs live in the native buffer rather than in your JS context — see Breadcrumbs.

Hermes mapping applies to JavaScript frames only. Native frames use uploaded Apple dSYMs and Android native libraries.