Source Maps

How to Read Android Crash Logs and Logcat Traces

Decode Android crash logs: logcat output, Java stack traces, native tombstones, and ANR traces — plus how to deobfuscate release builds that ship with R8.

When an Android app crashes in production, the error message you get in Google Play Console is rarely the full story. Behind the scenes, logcat is printing a detailed trace, your native code may have died hard with a segmentation fault, and the main thread might have been frozen for so long the system gave up. Learning how to read an android app crash log — whether it's a Java exception, a native crash, or an ANR — is the difference between a wild goose chase and a ten-minute fix. This guide walks you through the three distinct kinds of Android crash output, shows you how to extract signal from noise, and teaches you the tools and techniques to deobfuscate production crashes.

Android crashes come in three flavors: Java/Kotlin exceptions in logcat, native crashes that produce tombstones, and ANRs (Application Not Responding). Each looks different, each has its own roadmap, and each requires a different strategy to decode. Like reading a Java stack trace, the process demands a steady hand and a clear methodology. By the end of this post, you'll know where to look, what to ignore, and how to turn a cryptic crash log into actionable code.

Java and Kotlin exceptions in logcat

The most common crash is a Java or Kotlin exception. It lands in logcat as a FATAL EXCEPTION block with an AndroidRuntime tag. Here's a real example:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.myapp, PID: 12345
    java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.isEmpty()' on a null object reference
        at com.example.myapp.LoginActivity.validateEmail(LoginActivity.java:45)
        at com.example.myapp.LoginActivity.onCreate(LoginActivity.java:30)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3700)
        ... 10 more

Let's decode this:

The error message ("Attempt to invoke virtual method...on a null object reference") tells you what went wrong. The package name (com.example.myapp) and PID identify the process. The stack trace below shows the call chain.

Read the stack frames from top to bottom, focusing on your package. Here, the error occurred at LoginActivity.validateEmail (line 45). The frame below it shows that onCreate (line 30) called validateEmail. The frames with android.app are framework code; ignore them unless you're debugging something very specific.

The real culprit is usually the first frame in your package that called something else. In this case, validateEmail passed null where a String was expected, and onCreate called validateEmail with bad data (or no error handling).

Using adb logcat effectively

To capture this crash live from your device:

adb logcat | grep -i "fatal exception"

To filter by app package:

adb logcat --pid=$(adb shell pidof com.example.myapp)

To filter by priority (errors and warnings only):

adb logcat *:E

To clear the log and capture a fresh crash:

adb logcat -c
# Trigger the crash, then
adb logcat > crash.log

For a full device diagnostic (logcat, tombstones, battery stats, everything):

adb bugreport

This produces a .zip file with all system logs — invaluable when the app crashes in a way you can't easily reproduce.

Pipe logcat output to a file while testing; don't try to read it in real-time if the app logs aggressively. Logcat wraps old messages, so if your app spams the log, the crash will scroll off the top.

Native crashes and tombstones

Not all crashes happen in Java. If your app uses the NDK (native code via JNI), a segfault in C or C++ will crash the entire process — no NullPointerException, no try-catch. Instead, you get a tombstone: a system dump of the crashed process.

Look for the marker >>> com.example.myapp <<< in logcat. That's the start of a native crash dump:

F/libc: Signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xdeadbeef
    >>> com.example.myapp <<<
    backtrace:
        #00 pc 0x0001a234  /data/app/com.example.myapp-xyz/lib/arm64-v8a/libnative.so (imageProcessing+0x1234)
        #01 pc 0x0002b567  /data/app/com.example.myapp-xyz/lib/arm64-v8a/libnative.so (processFrame+0x5678)
        #02 pc 0x00001abc  /data/app/com.example.myapp-xyz/lib/arm64-v8a/libnative.so (Java_com_example_myapp_NativeLib_onFrameReceived+0x9ab)
        #03 pc 0x0008f012  /system/lib64/libart.so (art::JniAbort+0x12)

SIGSEGV means segmentation fault (accessing invalid memory), SIGABRT means the app aborted itself, SIGPIPE means a pipe was closed unexpectedly. The fault addr shows the bad memory address (often a sign of a buffer overflow or use-after-free).

Each frame shows: #NN pc <offset> <.so path> (symbol+offset). The pc is the program counter (the instruction offset in the binary). To get readable function names and line numbers, you need to symbolicate this dump using ndk-stack or addr2line with the unstripped .so files from your build's obj/local/<abi>/ directory. If your app uses native libraries heavily, native crash symbolication is essential for any production environment.

Symbolicating with ndk-stack

adb bugreport > report.zip
unzip report.zip
# Extract logcat from the archive
ndk-stack -sym path/to/obj/local/arm64-v8a < *.log

This rewrites the backtrace, replacing offsets with actual function names and line numbers. Without symbolication, a crash like:

#00 pc 0x0001a234  libnative.so (imageProcessing+0x1234)

becomes:

#00 pc 0x0001a234  libnative.so
    com/example/myapp/nativelib.cpp:42

Now you can jump to line 42 of your source and see what went wrong.

The .so file used for symbolication must be the exact unstripped binary that was in the app at crash time. A recompiled or release build's .so will produce nonsense offsets. Store unstripped binaries (or a mapping of offsets to symbols) for every release.

ANRs (Application Not Responding)

An ANR occurs when the main thread is blocked for more than 5 seconds (roughly). The system kills the app and saves a trace to /data/anr/traces.txt on the device. This trace doesn't crash the app; it just looks frozen to the user.

Pull the ANR trace:

adb pull /data/anr/traces.txt

Inside, you'll find a human-readable snapshot of all threads, their stack frames, and what they're holding. Search for your app's process:

----- pid 12345 at 2025-07-14 10:30:45 -----
Cmd line: com.example.myapp

DALVIK THREADS (N=42):
"main" prio=5 tid=1 Native
  | group="main" sCount=1 ucsCount=0 me=0x7fb2a3e4c0 peer=0x12d40000 state=S
  #00 pc 0x00012345 /system/lib64/libcutils.so (some_system_call+0x45)
  at com.example.myapp.APIClient.fetchUserProfile(APIClient.java:120)
  at com.example.myapp.MainActivity.onCreate(MainActivity.java:35)

Look for the main thread (marked "main" with tid=1). Its stack shows what it was doing when the ANR fired. Often you'll see it's blocked waiting for a lock, network call, or disk I/O.

The key insight: the main thread must never block. Understanding what blocks the main thread is the first step to preventing ANRs. If it's sleeping, waiting for a network response, or synchronizing a file, the UI freezes. Move I/O to a background thread. Use coroutines, Thread, or an Executor:

// Bad: blocks the main thread
val data = httpClient.fetchUserProfile() // This can take seconds

// Good: off the main thread
viewModelScope.launch(Dispatchers.IO) {
    val data = httpClient.fetchUserProfile()
    withContext(Dispatchers.Main) {
        updateUI(data)
    }
}

Play Console's Android Vitals tab shows your app's ANR rate (crashes are reported separately). An ANR rate above 0.5% is a red flag; anything above 1% is critical. Compare your ANR rate against Android Vitals benchmarks to see how you stack up.

Deobfuscation: mapping.txt and retrace

When you build a release APK or AAB, R8 and ProGuard minify and rename classes, methods, and variables to save space. A production crash trace looks like:

java.lang.NullPointerException
    at a.b.c.d(Unknown Source:45)
    at a.b.c.e(Unknown Source:123)

Useless. The file mapping.txt (produced at build time) is a reverse dictionary that maps a.b.c back to com.example.myapp.LoginActivity. Every release build produces a new mapping file; you must keep them all.

To deobfuscate, use the R8/ProGuard retrace tool:

cat obfuscated_stack_trace.txt | retrace.sh mapping.txt

Or with the Gradle plugin:

./gradlew retrace --stacktrace=obfuscated_stack_trace.txt

This rewrites the trace to readable class and method names. The output tells you exactly which line of your source crashed.

Store mapping.txt alongside every release. If you lose it, the stack trace is permanently unreadable. Many teams store it in version control (tagged by build number) or upload it to their error tracker. LightTrace integrates with the Sentry Android SDK, which automatically collects stack traces from your app via the standard Sentry protocol — change only the dsn to point at LightTrace, and crashes land in a dashboard where you can deobfuscate them and trace back to GitHub source in one click.

Reading crashes from Play Console and Google Cloud Logging

When your app crashes on a user's device, Google Play Console aggregates the crash in the Crashes and ANRs tab, grouped by fingerprint. The console shows:

  • Crash frequency — how many times it's been reported.
  • Affected users — how many unique users hit it.
  • Android version, device, API level — where it's most common.
  • Stack trace — usually obfuscated; you'll need to deobfuscate it yourself.

The trace is often truncated or rewritten by the Play Console backend, so cross-reference it with logcat if possible. If the crash is reproducible, capture it live via adb logcat — that's the most reliable source.

For deeper logging and structured crash data, set up LightTrace with the Sentry Android SDK. The SDK captures crashes, ANRs, native errors, and breadcrumbs (the sequence of events before the crash), then sends them to LightTrace. You'll see deobfuscated traces, affected users, releases, and integration with your GitHub source links.

Start tracking errors in minutes

When you're hunting an Android crash, LightTrace receives and deobfuscates your traces via the Sentry Android SDK, shows you affected users and releases, and links each frame to your GitHub code — set up the SDK in minutes and start shipping faster.

Crashing is not shameful; shipping crashes to production is. Master these three crash types, learn your tools (logcat, retrace, Play Console), and you'll navigate from "what broke?" to "it's fixed" in minutes, not hours. The framework gives you the signals; you just need to know how to read them.

Fix your next production error faster

Point any Sentry SDK at LightTrace — free up to 5,000 events/month.