This is an Android crash log:

--------- beginning of crash
AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.developer.crashsample, PID: 3686
java.lang.NullPointerException
	at com.android.developer.crashsample.ComposableSingletons$MainActivityKt.lambda$0(MainActivity.kt:27)
	at androidx.compose.foundation.ClickableNode.handleUpEvent(Clickable.kt:958)
	...
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:918)

That is Android’s own example from its crash documentation. Android crash logs live in more places than that one, though, and the right one depends on what failed. A Java or Kotlin exception goes to logcat’s crash buffer, which adb logcat -b crash prints. An ANR leaves a trace in /data/anr. A native crash prints a short dump to logcat and writes a full tombstone to /data/tombstones/. On a user’s phone you get none of these directly: you get Play Console’s Android vitals, a crash reporter, or, from Android 11, the app asking the OS why its last process died.

This guide covers where each one lives, how to get it, and how to read it, including the obfuscated traces an R8 release build produces. It is for native Android apps and the Android side of React Native apps. For iOS this is the wrong page. For Roku, see Roku crash logs.

Which crash log you need

Decision flowchart for picking an Android crash log: on a device you can plug in, a Java or Kotlin exception goes to the logcat crash buffer, an ANR to the /data/anr traces or a bug report, and a native signal to a tombstone symbolized with ndk-stack; on a user's phone, the sources are Play Console Android vitals and a crash reporter; any obfuscated trace is retraced with that build's mapping.txt.Decision flowchart for picking an Android crash log: on a device you can plug in, a Java or Kotlin exception goes to the logcat crash buffer, an ANR to the /data/anr traces or a bug report, and a native signal to a tombstone symbolized with ndk-stack; on a user's phone, the sources are Play Console Android vitals and a crash reporter; any obfuscated trace is retraced with that build's mapping.txt.
FailureWhere Android writes itHow you get it
Java or Kotlin exceptionlogcat crash bufferadb logcat -b crash, or Android Studio’s Logcat with is:crash
ANR/data/anr/anr_* files, /data/anr/traces.txt on older releasesadb root, then adb pull /data/anr/<file>; or a bug report
Native crashShort dump in logcat, full tombstone in /data/tombstones/A bug report, or the logcat dump fed to ndk-stack
Any of the above, after the factThe OS exit record, Android 11+ActivityManager.getHistoricalProcessExitReasons from your own app
Crashes on users’ phonesGoogle PlayPlay Console: Monitor and improve > Android vitals > Crashes and ANRs

Logcat: the crash buffer

Logcat is where a Java or Kotlin crash log shows up first. Android keeps several circular log buffers, and crash logs have their own:

adb logcat -b crash

adb logcat is shorthand for adb shell logcat, and -b picks the buffer. With no -b, logcat shows the main, system and crash buffers together. The main buffer on its own does not contain crash messages. A tool or script that reads only main will miss the crash it is looking for. To read several buffers at once, list them: adb logcat -b main,system,crash.

The buffers are circular, so pull the log soon after the crash. On a busy device, newer output pushes older entries out.

In Android Studio, the Logcat window shows the exception and its stack trace with links into your code. Two queries do most of the work. is:crash matches Java and native app crashes. package:mine limits output to the packages in the open project. When your process dies and restarts, Logcat prints PROCESS ENDED and PROCESS STARTED, so you can see where one run ended.

How to read a Java or Kotlin stack trace

Here is an illustrative crash. The package, classes and line numbers are made up:

FATAL EXCEPTION: main
Process: com.example.shop, PID: 4120
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.shop/com.example.shop.CheckoutActivity}: java.lang.IllegalStateException: cart not loaded
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:...)
	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:...)
	...
Caused by: java.lang.IllegalStateException: cart not loaded
	at com.example.shop.CartRepository.requireCart(CartRepository.kt:58)
	at com.example.shop.CheckoutActivity.onCreate(CheckoutActivity.kt:31)
	at android.app.Activity.performCreate(Activity.java:...)
	... 12 more

Read it in this order:

  1. The exception type. Android's crash guide calls it a strong hint about what went wrong. Look up the class before you look at the code.
  2. The last "Caused by". When one exception wraps another, the trace prints the original under a "Caused by:" header. Read the bottom one first. In this example, the top line only says the activity failed to start, and the cause says why.
  3. The first frame in your package. Under that cause, the first "at" line is where the exception was thrown. Here it is CartRepository.kt:58, called from CheckoutActivity.onCreate. Start there and walk up the stack to find who passed the bad value.
  4. Ignore "... 12 more". It means the rest of that cause's trace matches the frames of the exception that wraps it, so nothing is hidden.

If none of your code appears in the trace, Android’s crash guide says you probably passed an invalid parameter into an asynchronous operation. Check the framework classes the trace names and what you handed them.

A trace from somewhere else, such as a Play Console report or a tester’s message, opens in Android Studio with Analyze > Analyze Stacktrace. Paste it in and the frames link into your project. Check out the same version of the app that crashed first. Otherwise the file names and line numbers will not line up.

ANR traces

An ANR is not an exception, so there is no “Caused by” to read. Android raises one when your app has not responded to an input event within 5 seconds, when a BroadcastReceiver has not finished in time (10 seconds for a broadcast sent with FLAG_RECEIVER_FOREGROUND and 60 seconds without it, through Android 13; 10 to 20 and 60 to 120 seconds from Android 14, longer when the process is CPU-starved — Android’s timeout table), or when a service started with startForegroundService does not call startForeground within 5 seconds.

Android stores ANR traces in /data/anr: one traces.txt file on older releases, several anr_* files on newer ones. Reading them needs adb as root:

adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>

On a physical device, Android’s ANR guide points to a bug report instead, from the Take bug report developer option or adb bugreport. In the trace, find the main thread and read what it was waiting on when the clock ran out.

Native crashes: the Android crash dump and the tombstone

When native code crashes, Android writes two things. A basic crash dump goes to logcat, starting with a line of asterisks. A tombstone goes to /data/tombstones/, with stack traces for every thread in the process, a full memory map and the list of open file descriptors. The last line of the logcat dump gives the tombstone’s path. Since Android 8.0, crash_dump32 and crash_dump64 write them. Before that, the debuggerd daemons did.

An illustrative dump. The process and library names are made up:

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/foo/bar:10/123.456/78910:user/release-keys'
ABI: 'arm64'
pid: 4120, tid: 4120, name: com.example.shop  >>> com.example.shop <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Cause: null pointer dereference
backtrace:
      #00 pc 000000000001a2b4  /data/app/.../lib/arm64/libshop.so
      #01 pc 000000000001a0f0  /data/app/.../lib/arm64/libshop.so

Read the header first:

In the backtrace, frame #00 is where execution was when the signal arrived. Frames from a stripped release library have no function names. That is what symbolizing fixes.

ndk-stack replaces each address inside your libraries with a source file and line number. It needs the unstripped libraries from the same build. For a Gradle build they are under <project>/build/intermediates/cxx/<build-type>/<hash>/obj/<abi>:

adb logcat | $NDK/ndk-stack -sym <path-to-unstripped-libs>/arm64-v8a

It also accepts a saved file with -dump. It starts parsing at the line of asterisks, so keep that line when you copy a dump out of logcat.

To get the full tombstone off a device, take a bug report. The report zip copies device files under an FS folder, and AOSP’s own debugging guide runs its symbolizer on FS/data/tombstones/tombstone_05. What else the zip holds, and how to capture one from Developer options, the emulator or adb bugreport, is in Android bug report: what it captures and what it misses.

ApplicationExitInfo: ask the OS why the app died

Everything above needs a device on your desk. From Android 11 (API level 30), the app can ask the OS itself:

val am = getSystemService(ActivityManager::class.java)
val exits = am.getHistoricalProcessExitReasons(packageName, 0, 5)
for (exit in exits) {
    when (exit.reason) {
        ApplicationExitInfo.REASON_CRASH -> { /* unhandled Java or Kotlin exception */ }
        ApplicationExitInfo.REASON_CRASH_NATIVE -> { /* native crash */ }
        ApplicationExitInfo.REASON_ANR -> { /* ANR: exit.traceInputStream has the trace */ }
        ApplicationExitInfo.REASON_LOW_MEMORY -> { /* low memory killer */ }
    }
}

A pid of 0 returns every matching record, newest first. Each record says why the process ended and when. For an ANR, getTraceInputStream() returns the trace the system took. From API 31, it returns the tombstone for a native crash, as a protobuf.

Three limits shape how you use it:

  1. It is a ring buffer. Only the most recent exits are kept. Read them on the next launch, not a week later.
  2. Traces can be missing. They sit in a global circular buffer that newer crashes, including other apps' crashes, can overwrite. getTraceInputStream() can return null.
  3. Low-memory kills are not always labelled. On devices that do not support reporting REASON_LOW_MEMORY, a memory-pressure kill comes back as REASON_SIGNALED with SIGKILL.

A crash reporter that reads this record on the next launch can report ANRs and native crashes without running anything at the moment of death. That is also why such reports arrive late.

DeadSystemException: the crash that is not yours

Sometimes the crash log names android.os.DeadSystemException, or a RuntimeException wrapping one. Android’s reference defines it in two sentences: “The core Android system has died and is going through a runtime restart. All running apps will be promptly killed.”

It was added in API level 24 and extends DeadObjectException. When your app calls a system service and system_server has died, the framework throws a DeadSystemRuntimeException with a DeadSystemException inside. AOSP’s source comment is blunt: “There’s nothing apps can do at this point - the system will automatically restart - so there’s no point in catching this.”

So the stack trace points at your call into the system, but the cause is the system process. Look at the logs from before it, filter these crashes into their own group, and check whether they cluster on one device model or OS version. A plain DeadObjectException from your own bound service is different. AOSP’s guidance there is to drop the binder, clean up, and reconnect.

Obfuscated traces: R8, mapping.txt and retrace

A release build shrunk by R8 produces traces like at a.b.c(Unknown Source:4). R8 can rename classes and methods and change line numbers, so the trace no longer refers to your source. Each build writes a mapping.txt that maps the new names back.

Run R8 retrace with the mapping from the build that crashed. Android’s docs give this form, from the project root:

$ANDROID_HOME/cmdline-tools/latest/bin/retrace \
  app/build/outputs/mapping/release/mapping.txt trace.txt

The syntax is retrace path-to-mapping-file [path-to-stack-trace-file]. Leave out the trace file and it reads standard input, so you can paste a trace in. It ships with the command-line tools package from version 4.0. From Android Studio Otter 3 Feature Drop and AGP 9.0, the Logcat window retraces stack traces by itself.

Android Studio overwrites mapping.txt on every build. Android's own docs tell you to save a copy each time you publish a release. A trace from version 41 cannot be retraced with version 42's mapping. Archive the file next to the build, or let your crash reporter upload it.

Google Play handles this for you if you ship an app bundle built with AGP 4.1 or later, because it reads the mapping from the bundle. For an APK, upload the file in Play Console’s App bundle explorer. Only crashes that arrive after the upload are deobfuscated. Native frames need native debug symbols instead: set android.defaultConfig.ndk.debugSymbolLevel to FULL in the app’s Gradle file, or SYMBOL_TABLE if the file goes over Play’s 800 MB limit.

React Native R8 builds

In a React Native app, R8 shrinking is a one-line switch in android/app/build.gradle:

def enableProguardInReleaseBuilds = true

React Native’s docs warn that it often needs configuration for each native library, in app/proguard-rules.pro. Once it is on, the Java and Kotlin frames in your Android crash logs (your native modules and the libraries under them) are obfuscated like any other R8 build, and mapping.txt plus retrace reads them the same way. JavaScript frames come from the JavaScript bundle and are mapped with its source map instead.

Production: why device logs stop working

Every method above assumes you can reach the device, and production crashes happen on phones you will never see. logcat needs a USB or wireless adb connection. /data/anr needs root. The tombstone is gone by the time the user tells you about it, if they ever do. Android error reporting in production is a different set of tools: each Android crash report comes from a phone you never touch.

Play Console Android vitals

Crashes and ANRs from users who share diagnostics data, grouped into clusters. Nothing to install.

A crash reporter SDK

Runs in your app, records each crash on the device, and sends it on the next launch with your own context attached.

ApplicationExitInfo

The OS's own exit record on Android 11 and later, readable by your app on the next launch.

Android vitals is in Play Console under Monitor and improve > Android vitals > Crashes and ANRs. It groups crashes into clusters that may share a root cause. It uses data only from users who agreed to share usage and diagnostics data. Between it and Crashlytics, it is the only one with ANRs from Android 10 and below, because those come from the OS rather than the next launch. Third-party reporters such as Sentry cover that range with a watchdog thread instead, a heuristic that can report false positives (Sentry). Vitals also affects distribution. Its bad behavior thresholds are a user-perceived crash rate of 1.09% overall or 8% on any one phone model, and a user-perceived ANR rate of 0.47% overall or 8% per phone model. Play uses the last 28 days of data. Over a threshold, Play may reduce your app’s visibility or show a warning on its store listing.

A crash reporter fills the gaps vitals leaves: users who did not opt in, your own breadcrumbs and user IDs, and alerts you control. Firebase Crashlytics, part of Google’s Firebase, is one. Once set up, it monitors an Android app for crashes, non-fatal errors and ANRs. Native crashes need its separate NDK setup. Its Gradle plugin uploads the R8 mapping file on its own. Its ANR coverage has a documented edge: Crashlytics reports ANRs only from Android 11 and higher, and only when the app next starts, while Android vitals sends ANR data after the ANR occurs. Expect the two ANR counts to differ.

Where Everframe fits

Everframe is one of those crash reporters, with a different emphasis. Its Android SDK files crashes into the same report format as the bug reports your testers file, so a crash and a human report about the same screen read the same way.

What it does on Android, per the Crash reporting docs:

What it does not do. On Android 7 to 10 it reports Java and Kotlin exceptions only: no native crashes, no ANRs, no low-memory kills. For those devices, Play Console's vitals is your source. A crash report has no screenshot and no replay, because the handler runs in a dying process. The SDK reads Timber output plus System.out and System.err, not the logcat buffer, so plain android.util.Log lines are left out of reports (see Console & network in the Android docs). Native crash and ANR capture runs in your main process only, and a crash in the moment before the SDK finishes its background setup is not reported. Reports also arrive on the next launch, so a crash can show up days after it happened.

If you want to try it next to what you already run, the Free plan covers 1,000 monthly active installs with unlimited crash reports. Setup is two Maven artifacts and one start call, described on the Android SDK page.

What to do with a crash log, in order

  1. Find the right log. A Java or Kotlin exception is in the logcat crash buffer, an ANR in /data/anr, and a native crash in a tombstone. For users' phones, use vitals or your crash reporter.
  2. Retrace it. If you see names like a.b.c, run retrace with the mapping.txt from that exact build. For native frames, use ndk-stack with the unstripped libraries.
  3. Read the last cause, then your first frame. The exception type tells you what failed. The first frame in your package tells you where.
  4. Rule out the system. DeadSystemException means the OS restarted under you. Group those crashes separately instead of debugging your own code.
  5. Keep every release's mapping.txt and symbols. Without them, next month's crash log is unreadable.