Two crashes dominate the Android Vitals dashboard for almost every app: the Application Not Responding dialog (ANR) and OutOfMemoryError (OOM). They have different causes — one is about time, the other about memory — but they share a root: work happening in the wrong place. This guide shows how to read the trace files each produces, find the cause, and keep your main thread responsive and your heap under control.
What ANRs and OutOfMemoryErrors actually are
An ANR fires when your app can't respond to input for too long — the system throws up the "isn't responding" dialog. The usual triggers:
- The main (UI) thread is blocked for ~5 seconds on input dispatch.
- A
BroadcastReceiverdoesn't finish within its timeout. - A
Servicedoes heavy work on the main thread.
An OutOfMemoryError is thrown when the Dalvik/ART heap can't satisfy an allocation. On Android this is almost always large bitmaps or a memory leak that keeps objects alive across configuration changes.
ANRs aren't exceptions you can try/catch — they're detected by the system watchdog. That's why you need them reported from the field: you can't reproduce every device, OS version, and thermal state in the lab.
Reading the trace: ANR traces and tombstones
When an ANR happens, Android dumps every thread's stack to /data/anr/traces.txt. Pull it with adb:
adb bugreport anr-report.zip
# or, on a rooted/debug device:
adb shell cat /data/anr/traces.txt
Find the main thread and read what it was doing. A blocked main thread parked in a database query, a network call, or SharedPreferences.commit() is your culprit. Reading these frames is the same skill as reading any stack trace — start at the top frame and walk down to your own code.
Native crashes and some OOMs produce a tombstone in /data/tombstones/, with the faulting signal (SIGSEGV, SIGABRT) and a native backtrace. You'll need symbols to make those readable — the same de-obfuscation problem that source maps solve for JavaScript, handled here by ProGuard/R8 mapping files and native symbol uploads.
Common causes of ANRs
Every ANR traces back to the main thread doing something it shouldn't. Move it off:
// ❌ Blocks the UI thread
val user = db.userDao().findById(id)
render(user)
// ✅ Off the main thread with coroutines
lifecycleScope.launch {
val user = withContext(Dispatchers.IO) { db.userDao().findById(id) }
render(user) // back on Main automatically
}
The worst offenders, in order: synchronous disk I/O (databases, SharedPreferences), network on the main thread, large JSON parsing, and bitmap decoding. Add StrictMode in debug builds to catch them early:
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()
)
Common causes of OutOfMemoryError
Most OOMs are bitmaps. A 12-megapixel photo decoded at full size is ~48 MB in memory — a few of those and you're done. Downsample when decoding:
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeResource(resources, R.drawable.photo, options)
options.inSampleSize = calculateInSampleSize(options, reqWidth = 1080, reqHeight = 1080)
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.photo, options)
The other cause is leaks: an Activity retained by a static reference, an inner-class Handler, or a listener never unregistered. Each leaked Activity drags its whole view tree along. The mechanism is identical to a JavaScript memory leak — something holds a reference that should have been released — and LeakCanary is the Android tool of choice for finding them.
Bitmap.recycle() and manual memory tricks are usually the wrong fix. Fix the cause — decode smaller, cache with an LRU, and release references — rather than fighting the garbage collector.
Catching them in production
You will never see most of these crashes locally. They happen on a specific device, at a specific memory pressure, for a specific user. That's what Android error tracking is for: point the Sentry SDK at LightTrace and every ANR and OOM arrives with the thread dump, the device and memory context, and the breadcrumb trail of what the user did first.
Watch the aggregate, too. A spike in ANRs after a release shows up immediately in your crash-free rate, which is the single number that predicts your store rating. When it dips, you know which release to look at — and often which line.
Start tracking errors in minutes
See every Android ANR and OutOfMemoryError with the thread dump and device context attached — point the Sentry SDK at LightTrace and track crash-free rate per release.
ANRs and OOMs feel like device problems, but they're almost always code doing the wrong work in the wrong place. Move it off the main thread, decode bitmaps small, release your references — and let production reporting tell you the moment a regression ships.