Skip to content
Everframe Docs
Documentation

Crash reporting

Native crashes on iOS and tvOS become reports on their own, sent on the next launch. Upload dSYMs to read their stacks.

Updated

Crash reporting is on by default. Everframe.shared.start is enough; there is nothing else to install. The crash recorder is part of the SDK, and the same SDK captures crashes on iPhone, iPad and Apple TV.

It is a client veto. One switch turns it off:

EverframeConfig(sdkKey: "evf_live_…", capture: CaptureConfig(crash: false))

What is captured

The SDK records these as crash reports:

  • Swift runtime traps: fatalError, a failed precondition, force-unwrapping nil, an index out of range, try! on an error. When the Swift runtime provides a message and source location, the report carries them.
  • Uncaught Objective-C exceptions (NSException), with the exception’s name and reason.
  • Memory faults: an invalid pointer or a write to protected memory (EXC_BAD_ACCESS, SIGSEGV, SIGBUS).
  • abort() and other fatal signals: SIGABRT, SIGILL, SIGFPE, SIGTRAP.

An uncaught C++ exception ends in abort(), so it is recorded as SIGABRT without the C++ exception’s type or message.

iOS and tvOS

On iOS, a Mach exception handler on its own thread records traps and memory faults, including a stack overflow. Signal handlers record the rest, such as abort().

tvOS uses the same SDK with one difference: there the SDK has no Mach exception handler and no alternate signal stack. Signal handlers record every fault on the crashing thread’s own stack. Everything in the list above is captured on tvOS. A stack overflow is not, because a thread that has run out of stack cannot run the handler.

Delivery is on the next launch

A crashed process cannot send anything. The SDK writes the crash to disk as it happens. The next time your app calls start() with crash capture on, the SDK moves the crash into its encrypted outbox and sends it with the launch upload. A failed upload is retried like any other report.

The report keeps the context of the run that crashed: its app version, device details, the user you set with setUser, and the project it reports to. A different user signing in on the next launch does not change it. occurredAt is the time of the crash, not the time of the upload.

Capture begins a moment after start(), once the SDK has saved that context in the background. A crash before start(), or in that moment, is not reported. Calling setUser or start() again opens the same short gap while the new context is saved.

What arrives

A native crash arrives as a report with source: "crash", handled: false and fatal: true. mechanism says which handler caught it: native-mach, native-signal or native-nsexception. exceptionType is the Mach exception, signal or exception name, such as EXC_BAD_ACCESS, SIGABRT or NSInvalidArgumentException.

The stack is a list of addresses, with the UUID and load address of each binary image they fall in. That is what lets Everframe match the frames to your dSYMs.

A native crash report has no description, screenshot, replay, console output or breadcrumb trail. It also carries no verified identity from setIdentityToken, only the user you set with setUser.

Readable stacks need dSYMs

Release builds strip symbols from the binary, so the SDK sends addresses. To read them as file and line, upload the dSYMs of the exact build you ship. Swift function names currently stay in their mangled form. Everframe matches dSYMs by image UUID and architecture, never by version number, so a dSYM from another build is never used. The Xcode build phase in Apple dSYMs uploads them with every Release build and archive, and CI can upload an archive instead.

You can upload after the crash has arrived. Once the matching dSYM is ready, Everframe maps the reports it already holds. Until then the error detail shows dSYM missing and the raw stack. Once mapped, the stack opens in Mapped mode, and Raw still shows the captured frames. Apple’s system libraries have no dSYMs to upload, so the status can say Partially mapped even when every frame from your app is mapped.

Grouping

Crashes are grouped by the mapped function of your app’s frames, so the same crash in a new build joins its existing issue once that build’s dSYMs are uploaded. Until they are, it waits in an issue of its own for that build.

Out-of-memory and watchdog terminations

iOS and tvOS end a process that uses too much memory (jetsam) or stops responding (the watchdog) with SIGKILL. No crash handler can record that signal, so there is never a stack. On the next launch the SDK checks what the previous process left behind and, when it was killed in the foreground, sends one report for it:

  • a fatal crash with no frames, mechanism apple-termination-inference, and a message that says inferred;
  • the cause: Low memory kill (a memory warning or critical memory pressure in the last 60 seconds, or 20 % or less of the memory limit left at the last sample), Unresponsive termination (the main thread stalled for at least 5 seconds), or Abnormal foreground termination (neither);
  • the last sampled memory footprint and available memory in KiB, the number of memory warnings, the memory pressure level, the main-thread stall and the thermal state, sampled every 5 seconds while the app is not in the background.

A termination is inferred only when all of these held for the previous process: it left no crash report, it did not exit through exit() or applicationWillTerminate, no debugger was ever attached, crash capture was armed, the app version, build and executable were the same, the OS version was the same, the device did not restart, the app was active in the foreground (or inactive with a stalled main thread), and it was last seen within 14 days.

Each cause is one issue per project, shared by iOS and tvOS, labelled Inferred in the dashboard, and never merged with Android’s OS-confirmed low-memory kills. On iOS, release health counts it as an other exit; it never lowers the crash-free rate.

The report follows capture.crash and needs no other API. It is anonymous: no user, session, breadcrumbs, logs or attachments. Not reported:

  • kills while the app is in the background (including background audio and picture in picture), and force-quits from the app switcher, which deactivates the app first;
  • kills during launch before capture is armed, and _exit();
  • app extensions, Mac Catalyst and iOS apps running on a Mac;
  • the iOS and tvOS simulators, unless the launch environment sets EVERFRAME_SIMULATOR_TERMINATION_INFERENCE=1.

The memory and watchdog thresholds are set by the OS and have not been qualified yet.

Turn it off

CaptureConfig.crash defaults to true. Setting it to false turns off native crash capture and explicit captureException together. If capture was never on in the process, the crash recorder is not installed. The SDK’s breadcrumb handler for uncaught exceptions still runs, and passes each exception on.

Crashes recorded by earlier runs stay on the device and are not sent while capture is off. kill() also stops capture for the running SDK. In a React Native app, the same switch covers the bridge’s JavaScript crash path.

Other crash reporters

The SDK installs fatal signal handlers, an uncaught-exception handler and, on iOS, a Mach exception handler. Each one passes the crash on to the handler that was installed before it, so a crash reporter that starts before Everframe still receives the crash. A reporter that starts after Everframe sees the crash first, and Everframe records it only if that reporter passes it on.

Running Everframe beside another crash reporter has not been qualified yet. If you keep one, check that both receive a crash from a Release build, or set capture.crash to false and leave crashes to the other reporter.

Limits

  • Memory and watchdog kills are inferred, not recorded. The system ends the app with SIGKILL for running out of memory (jetsam) or for hanging, and no handler inside the app can see it. Foreground kills are reported on the next launch without a stack, as described in Out-of-memory and watchdog terminations; background kills are not reported.
  • Stack overflows are not captured on tvOS.
  • Crashes under the debugger are usually not captured. With Xcode’s debugger attached, the debugger stops the app at the crash before Everframe can record it. To test crash reporting, stop the app in Xcode, open it from the Home screen, make it crash, then open it again.
  • Crashes before start() and in the moment after it are not captured.

Report a caught error

Errors you catch are not crashes, but you can report them:

import EverframeKit

do {
    try saveCart()
} catch {
    Everframe.shared.captureException(
        error,
        options: CaptureExceptionOptions(
            severity: .warning,
            context: "checkout.save-cart",
            metadata: ["attempt": 2]
        )
    )
}

The one-argument captureException(error) form defaults to error severity. Both forms record handled: true, fatal: false, and mechanism captureException without opening the reporter. The SDK takes a bounded, redacted snapshot, writes to its encrypted outbox on a best-effort basis, and returns before delivery is confirmed. capture.crash: false suppresses this path too.

Frames describe Thread.callStackSymbols at the capture call, not necessarily the original Swift throw site, and they are not mapped with dSYMs.