Production engineering guide for mobile crash reporting covering patterns, implementation strategies, and operational best practices.
Mobile crash reporting is the process of capturing, aggregating, and analysing application crashes across a fleet of devices, enabling engineers to detect, diagnose, and resolve issues before they impact users. It matters most when an app is live, especially on production devices where users encounter rare, intermittent, or device-specific failures that are hard to reproduce in development.
Crashes in mobile apps occur when an unhandled exception propagates to the runtime and terminates the process. The system records a crash report, which includes the stack trace, native and managed frames, thread state, and metadata such as device model, OS version, network conditions, and app version.
The first step in crash reporting is capturing the crash event at the moment of failure. On iOS, this begins with the NSException being thrown and not caught. The runtime calls NSSetUncaughtExceptionHandler, which triggers the crash handler. On Android, crashes are captured via Thread.setDefaultUncaughtExceptionHandler, which intercepts uncaught exceptions on all threads.
For a crash to be captured, the application must be instrumented to record the event. This is typically done using a crash reporting SDK that automatically registers handlers and collects metadata.
In Kotlin, the standard setup for capturing crashes involves registering a Thread.UncaughtExceptionHandler in Application.onCreate:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
val crashReport = mapOf(
"timestamp" to System.currentTimeMillis(),
"thread_name" to thread.name,
"app_version" to BuildConfig.VERSION_NAME,
"device_model" to Build.MODEL,
"os_version" to Build.VERSION.RELEASE,
"crash_stacktrace" to Log.getStackTraceString(throwable),
"crash_message" to throwable.message,
"crash_class" to throwable.javaClass.simpleName
)
sendCrashReport(crashReport)
}
}
}
The sendCrashReport function may serialize the data as JSON and upload it to an endpoint via HTTP.
Once a crash is captured, the report must be sent to a central system. The most common method is an HTTP POST to an endpoint, typically using a JSON payload.
A minimal crash report has this structure:
{
"timestamp": 1702732800000,
"thread_name": "main",
"app_version": "2.4.1",
"device_model": "iPhone14,3",
"os_version": "17.1",
"crash_message": "NullPointerException: Attempt to invoke virtual method 'void com.example.User.setName(java.lang.String)' on a null object reference",
"crash_class": "NullPointerException",
"crash_stacktrace": [
"com.example.MainActivity.onCreate(MainActivity.java:34)",
"android.app.Activity.performCreate(Activity.java:8274)",
"android.app.Activity.performCreate(Activity.java:8253)",
"android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1335)",
"android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3560)",
"android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3765)"
],
"crash_context": {
"user_id": "u12345",
"session_id": "s98765",
"network_type": "wifi",
"battery_level": 0.78,
"memory_usage_mb": 142,
"disk_free_mb": 1200
}
}
The crash report is sent via HTTP POST to /api/v1/crashes, with the following headers:
POST /api/v1/crashes HTTP/1.1
Host: crash-reporting.example.com
Content-Type: application/json
Authorization: Bearer abc123def456
X-App-Version: 2.4.1
X-Device-Model: iPhone14,3
X-OS-Version: 17.1
The server responds with a 201 Created status and a unique ID for the crash, which is stored in the client for future use.
Crash reports are often sent asynchronously, and the device may be offline when a crash occurs. Therefore, the SDK must persist crash data locally before sending.
On iOS, the NSFileCoordinator and NSKeyedArchiver are used to store crash reports in Application Support/CrashReports/. On Android, SharedPreferences and Room are common for storing crash data in data/data/com.example.app/files/crashes.
The SDK should implement a retry mechanism that attempts to send a crash report up to three times with exponential backoff. If a report fails to send, it is queued in a local store and retried on next app startup.
Failure modes include:
After reports are sent, they are aggregated and indexed for analysis. The backend system stores each crash in a database and builds a summary view.
A typical schema for a crash table in PostgreSQL:
CREATE TABLE crashes (
id UUID PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
app_version TEXT,
device_model TEXT,
os_version TEXT,
crash_class TEXT,
crash_message TEXT,
crash_stacktrace TEXT[],
context JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
The system indexes the following fields for fast querying:
crash_class (B-tree)app_version (B-tree)timestamp (B-tree)crash_message (GIN for full-text search)context (JSONB for key-value lookups)Crashes are grouped by a fingerprint, which is a hash of the crash class and stack trace. This allows the system to deduplicate identical crashes and surface the most common issues.
The fingerprint algorithm combines the class name and the first N frames of the stack trace. For example:
def fingerprint(crash):
stack = crash['crash_stacktrace']
# Use first 8 frames and class name
key = f"{crash['crash_class']}:{':'.join(stack[:8])}"
return hashlib.md5(key.encode()).hexdigest()
This fingerprint is stored in the crashes table and used to group crashes in dashboards.
With a fleet of devices, engineers must diagnose crashes at scale. The primary tool is a crash dashboard that lists the top N crashes by frequency, duration, and severity.
Each crash entry includes:
crash_contextTo diagnose a crash, engineers examine the stack trace and correlate it with the app’s code.
A common failure mode is misinterpreting the stack trace due to missing debug symbols. On iOS, crash reports include dSYM bundles, which map addresses to source code lines. On Android, mapping.txt files from ProGuard are used to deobfuscate stack traces.
The crash report must include the mapping.txt in the payload:
"crash_context": {
"mapping": {
"version": "2.4.1",
"mapping": [
"1234 -> 5678: com.example.MainActivity.onCreate",
"5678 -> 9012: com.example.User.setName",
"9012 -> 3456: com.example.UserRepository.save"
]
}
}
The backend system uses this mapping to generate a human-readable stack trace:
java.lang.NullPointerException: Attempt to invoke virtual method 'void com.example.User.setName(java.lang.String)' on a null object reference
at com.example.MainActivity.onCreate(MainActivity.java:34)
at android.app.Activity.performCreate(Activity.java:8274)
at android.app.Activity.performCreate(Activity.java:8253)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1335)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3560)
With ProGuard mapping applied:
java.lang.NullPointerException: Attempt to invoke virtual method 'void User.setName(String)' on a null object reference
at MainActivity.onCreate(MainActivity.java:34)
at Activity.performCreate(Activity.java:8274)
at Activity.performCreate(Activity.java:8253)
at Instrumentation.callActivityOnCreate(Instrumentation.java:1335)
at ActivityThread.performLaunchActivity(ActivityThread.java:3560)
Crash reports are more useful when correlated with user actions. The SDK collects events such as screen views, button taps, and API calls, and attaches them to the crash report.
For example, a crash occurs during a purchase flow. The event log shows:
[
{ "event": "screen_view", "screen": "Checkout", "timestamp": 1702732800100 },
{ "event": "button_tap", "button": "Pay Now", "timestamp": 1702732800500 },
{ "event": "api_call", "endpoint": "/api/v1/orders", "status": 500, "timestamp": 1702732800800 },
{ "event": "crash", "crash_id": "c12345", "timestamp": 1702732801100 }
]
The backend system links these events to the crash and displays them in the crash detail view. This allows engineers to reconstruct the user’s journey leading up to the crash.
Not all crashes are equally important. The system must prioritize crashes based on severity, frequency, and impact.
Crashes are prioritized using a scoring system that combines:
A crash is considered critical if it affects more than 5% of users and has a high occurrence rate (e.g., 100+ crashes per day).
The system automatically flags crashes that:
Crashes can be marked as resolved when a new app version includes a fix. This is done by comparing the app_version in the crash report with the version that includes the fix. The backend system queries:
SELECT id, crash_class, count(*) as occurrences
FROM crashes
WHERE app_version = '2.4.2'
AND timestamp >= '2023-12-15T00:00:00Z'
AND crash_class = 'NullPointerException'
GROUP BY id, crash_class
HAVING count(*) > 10;
Once a crash is diagnosed, the team resolves it by deploying a fix. The resolution is recorded in the crash report via a resolved_at timestamp.
The SDK can automatically send a “crash resolution” event when the app starts after a fix is deployed:
{
"event": "crash_resolved",
"crash_id": "c12345",
"resolved_at": 1702732802000,
"app_version": "2.4.2",
"resolution_notes": "Fixed User object not initialized in MainActivity.onCreate"
}
The backend system updates the crash record and sends a notification to the team.
Monitoring is done through a continuous process that checks for new crashes and alerts on thresholds:
The alert system uses curl to send notifications to Slack:
curl -X POST -H "Content-Type: application/json" \
-d '{
"text": "🚨 New crash: NullPointerException in MainActivity.onCreate",
"attachments": [
{
"title": "Affected devices",
"fields": [
{ "title": "App version", "value": "2.4.1", "short": true },
{ "title": "OS version", "value": "17.1", "short": true },
{ "title": "Users affected", "value": "1,243", "short": true }
]
}
]
}' \
https://hooks.slack.com/services/ABC123/DEF456/GHI789
cold_start flag in the context.crash_context is populated with data from different sources, but some values are missing or incorrect. For example, battery_level is missing on iOS due to missing UIDevice.current.batteryLevel.Mobile crash reporting is the foundation of stability in production mobile apps. It begins with capturing crashes at the moment of failure, sending them reliably over HTTP, aggregating them in a database, and diagnosing them using stack traces, context, and event correlation. The process requires attention to detail: proper error handling, persistence, retry logic, and fingerprinting. Engineers must be vigilant about common failure modes and use the system to prioritize and resolve issues at scale.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy