Android App Crash Patterns and Fixes
What are the most common Android app crash patterns and fixes?
TL;DR
- Bottom line: Most Android crashes fall into ~12 repeatable patterns; NullPointerException alone causes ~35% of all Java/Kotlin crashes, followed by ANRs from main-thread blocking, OOM from bitmap mismanagement, and IllegalStateException from fragment lifecycle violations.
- Key tool/command:
adb logcat *:E | grep -E "FATAL|ANR|Exception" - Watch out for: Catching generic
ExceptionorThrowable— this masks root causes and creates silent data corruption. - Works with: Android 5.0+ (API 21+), Kotlin 1.5+, Java 8+, Android Studio 2020.3+.
Constraints
- Native crashes (SIGSEGV/SIGABRT) require debug symbols — release builds strip symbols by default. Always upload native debug symbols to Play Console or Crashlytics.
- ANR thresholds are enforced by the OS: 5 seconds for input dispatch, ~5 seconds for foreground broadcast receivers, ~20 seconds for background receivers. These cannot be configured.
- Never catch
ThrowableorErrorin production — only catch specificExceptionsubclasses. CatchingOutOfMemoryErrorleads to undefined behavior. - Google Play flags apps exceeding 1.09% user-perceived crash rate, directly impacting search ranking and app discoverability.
TransactionTooLargeExceptionhas a hard 1MB Binder buffer limit per process — this cannot be increased.
Quick Reference
| # | Cause | Likelihood | Signature | Fix |
|---|---|---|---|---|
| 1 | NullPointerException | ~35% | java.lang.NullPointerException: Attempt to invoke virtual method '...' on a null object reference | Use Kotlin null safety (?., ?:); annotate Java with @NonNull/@Nullable |
| 2 | ANR — main thread I/O | ~15% | ANR in com.example.app + main thread in WAITING/BLOCKED | Move all disk/network I/O to coroutines (Dispatchers.IO) or WorkManager |
| 3 | OutOfMemoryError | ~10% | java.lang.OutOfMemoryError: Failed to allocate a N-byte allocation | Use Glide/Coil for images; set inSampleSize; avoid static bitmap references |
| 4 | IllegalStateException (Fragment) | ~8% | IllegalStateException: Can not perform this action after onSaveInstanceState | Use commitAllowingStateLoss() or check lifecycle.currentState.isAtLeast(STARTED) |
| 5 | SecurityException | ~6% | SecurityException: Permission Denial: ... requires android.permission.X | Check ContextCompat.checkSelfPermission() before calling protected APIs |
| 6 | SIGSEGV (native) | ~5% | signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 | Use AddressSanitizer; upload native debug symbols; check for use-after-free |
| 7 | TransactionTooLargeException | ~4% | TransactionTooLargeException: data parcel size N bytes | Keep onSaveInstanceState bundles < 500KB; use ViewModel for large state |
| 8 | IndexOutOfBoundsException | ~4% | IndexOutOfBoundsException: Index: N, Size: M | Validate list sizes; use getOrNull(); synchronize concurrent list modifications |
| 9 | DeadObjectException | ~3% | android.os.DeadObjectException at IPC call site | Wrap Binder calls in try/catch; reconnect on failure |
| 10 | ClassCastException | ~2% | ClassCastException: X cannot be cast to Y | Use is checks; prefer generics; use typed Bundle getters |
| 11 | SIGABRT (native) | ~2% | signal 6 (SIGABRT), code -6 (SI_TKILL) + FORTIFY: ... | Fix buffer overflows; review assert() conditions |
| 12 | ConcurrentModificationException | ~2% | ConcurrentModificationException during iteration | Use CopyOnWriteArrayList or toMutableList() before modification |
Decision Tree
START
├── Is it an ANR (dialog says "App isn't responding")?
│ ├── YES → Check main thread state in traces.txt
│ │ ├── Main thread BLOCKED on I/O → Move to Dispatchers.IO (Cause #2)
│ │ ├── Main thread WAITING on lock → Fix lock contention or deadlock
│ │ └── Main thread in long computation → Move to Dispatchers.Default
│ └── NO ↓
├── Is it a Java/Kotlin exception?
│ ├── YES → Read the exception class name
│ │ ├── NullPointerException → Cause #1: check null safety
│ │ ├── OutOfMemoryError → Cause #3: profile heap with Android Profiler
│ │ ├── IllegalStateException → Cause #4: check fragment/activity lifecycle
│ │ ├── SecurityException → Cause #5: check runtime permissions
│ │ ├── TransactionTooLargeException → Cause #7: reduce bundle size
│ │ ├── IndexOutOfBoundsException → Cause #8: validate bounds
│ │ ├── DeadObjectException → Cause #9: handle IPC failures
│ │ ├── ClassCastException → Cause #10: use type checks
│ │ └── ConcurrentModificationException → Cause #12: synchronize access
│ └── NO ↓
├── Is it a native crash (signal in logcat)?
│ ├── YES → Read the signal number
│ │ ├── SIGSEGV (signal 11) → Cause #6: null ptr / use-after-free in C/C++
│ │ ├── SIGABRT (signal 6) → Cause #11: assertion failure / buffer overflow
│ │ ├── SIGBUS → Misaligned memory access in native code
│ │ └── SIGFPE → Division by zero in native code
│ └── NO ↓
└── DEFAULT → Enable StrictMode + Crashlytics; reproduce with adb logcat filtering
Step-by-Step Guide
1. Capture the crash trace
Connect the device via USB and start logcat with crash filtering. [src1]
# Filter for fatal crashes and ANRs
adb logcat *:E | grep -E "FATAL EXCEPTION|ANR in|signal [0-9]+"
# Save full logcat to file for analysis
adb logcat -d > crash_log.txt
Verify: adb logcat -d | grep "FATAL EXCEPTION" → shows crash with full stack trace, thread name, PID.
2. Identify the crash category
Parse the first line of the exception to determine the crash type. Match against the Quick Reference table. [src7]
# Java/Kotlin crash format:
FATAL EXCEPTION: main
Process: com.example.app, PID: 12345
java.lang.NullPointerException: Attempt to invoke virtual method
'int java.lang.String.length()' on a null object reference
at com.example.app.MainActivity.processData(MainActivity.kt:42)
# Native crash format:
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
pid: 12345, tid: 12345, name: main >>> com.example.app <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Verify: You can identify the exception class, exact file/line number, and thread name from the trace.
3. Check if it's lifecycle-related
For crashes involving fragments, activities, or configuration changes, verify the component lifecycle state. [src6]
// Add lifecycle logging to narrow down timing
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val data = withContext(Dispatchers.IO) { repository.fetchData() }
if (viewLifecycleOwner.lifecycle.currentState
.isAtLeast(Lifecycle.State.STARTED)) {
binding.textView.text = data
}
}
}
}
Verify: adb logcat | grep "Lifecycle" — crash should not occur after DESTROYED state.
4. Profile memory for OOM crashes
Use Android Studio Profiler to capture heap dumps and track allocations. [src5]
# Dump heap from command line
adb shell am dumpheap com.example.app /data/local/tmp/heap.hprof
adb pull /data/local/tmp/heap.hprof
# Check process memory limits
adb shell getprop dalvik.vm.heapsize
Verify: Open .hprof in Android Studio Memory Profiler — look for retained Bitmap and byte[] allocations.
5. Retrieve ANR traces
Pull the ANR trace files to analyze which thread is blocked. [src2]
# List ANR trace files
adb shell ls /data/anr/
# Pull the most recent trace
adb pull /data/anr/traces.txt
# On Android 11+, use ApplicationExitInfo
adb shell dumpsys activity exit-info com.example.app
Verify: In traces.txt, find the main thread — its state (BLOCKED, WAITING) reveals the ANR cause.
6. Symbolicate native crashes
Use ndk-stack to convert raw addresses to source locations. [src3]
# Symbolicate from logcat output
adb logcat | ndk-stack -sym path/to/obj/local/arm64-v8a/
# From tombstone file
adb pull /data/tombstones/tombstone_00
ndk-stack -sym path/to/obj/local/arm64-v8a/ -dump tombstone_00
Verify: Output shows source file paths and line numbers instead of raw hex addresses.
Code Examples
Kotlin: Safe null handling patterns
// Input: Nullable data from Intent extras, Bundle, or Java interop
// Output: Crash-free access with sensible defaults
// Safe call + Elvis for default value
val userName: String = intent.getStringExtra("user_name") ?: "Guest"
// let-block for conditional execution
intent.getStringExtra("deep_link")?.let { url ->
navigator.handleDeepLink(url)
}
// require for fail-fast with clear messages
fun processOrder(orderId: String?) {
requireNotNull(orderId) { "orderId must not be null" }
repository.loadOrder(orderId) // smart-cast to non-null
}
Kotlin: Coroutine-safe lifecycle-aware operations
// Input: Async operations that outlive the Activity/Fragment
// Output: Automatic cancellation, no IllegalStateException
class SearchFragment : Fragment(R.layout.fragment_search) {
private val viewModel: SearchViewModel by viewModels()
override fun onViewCreated(view: View, s: Bundle?) {
super.onViewCreated(view, s)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { updateUI(it) }
}
}
}
}
Java: Defensive permission checking
// Input: Runtime permission request before camera access
// Output: Graceful handling without SecurityException crash
private void openCamera() {
if (ContextCompat.checkSelfPermission(this, CAMERA)
== PackageManager.PERMISSION_GRANTED) {
launchCamera();
} else if (shouldShowRequestPermissionRationale(CAMERA)) {
showPermissionRationale();
} else {
requestPermissions(new String[]{CAMERA}, REQ_CAMERA);
}
}
Java: Safe bitmap loading to prevent OOM
// Input: Large image file path
// Output: Downsampled bitmap that fits in memory
public static Bitmap decodeSampledBitmap(String path,
int reqW, int reqH) {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, opts);
opts.inSampleSize = calcSampleSize(opts, reqW, reqH);
opts.inJustDecodeBounds = false;
opts.inPreferredConfig = Bitmap.Config.RGB_565;
return BitmapFactory.decodeFile(path, opts);
}
Anti-Patterns
Wrong: Catching generic Exception to suppress crashes
// BAD — hides real bugs, causes silent data corruption
try {
val result = processPayment(order)
updateUI(result)
} catch (e: Exception) {
// Swallowed — no logging, no user feedback
}
Correct: Catch specific exceptions with proper handling
// GOOD — catches expected failures, lets real bugs surface
try {
val result = processPayment(order)
updateUI(result)
} catch (e: IOException) {
Log.w(TAG, "Network error", e)
showRetryDialog()
} catch (e: PaymentDeclinedException) {
showDeclinedMessage(e.reason)
}
Wrong: Network calls on the main thread
// BAD — causes ANR after 5 seconds
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val response = URL("https://api.example.com/data").readText()
binding.textView.text = response
}
Correct: Use coroutines with appropriate dispatcher
// GOOD — non-blocking, lifecycle-aware
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val response = withContext(Dispatchers.IO) {
URL("https://api.example.com/data").readText()
}
binding.textView.text = response
}
}
Wrong: Storing Activity references in static fields
// BAD — leaks the entire Activity, leads to OOM
companion object {
var currentActivity: Activity? = null
val cachedBitmap: Bitmap? = null
}
Correct: Use ViewModel or WeakReference
// GOOD — ViewModel survives config changes without leaking
class MainViewModel : ViewModel() {
private val _data = MutableLiveData<String>()
val data: LiveData<String> = _data
fun loadData() {
viewModelScope.launch { _data.value = repository.fetchData() }
}
}
Wrong: Fragment transaction after onSaveInstanceState
// BAD — crashes with IllegalStateException
fun onDataLoaded(data: Data) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, DetailFragment.newInstance(data))
.commit() // CRASH if after onSaveInstanceState
}
Correct: Check lifecycle state before transaction
// GOOD — guards against state loss
fun onDataLoaded(data: Data) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, DetailFragment.newInstance(data))
.commit()
} else {
pendingFragment = DetailFragment.newInstance(data)
}
}
Common Pitfalls
- Platform types from Java interop: Kotlin treats unannotated Java return values as platform types (
String!) — these bypass null safety. Fix: Add@Nullable/@NonNullannotations to all public Java APIs. [src1] - StrictMode not enabled during development: Many ANR-causing I/O operations go undetected until production. Fix:
StrictMode.setThreadPolicy(Builder().detectAll().penaltyLog().build())in debug builds. [src2] - Missing ProGuard/R8 mapping files: Crash reports show obfuscated stack traces (
a.b.c.d()). Fix: Uploadmapping.txtto Play Console and Crashlytics for every release. [src7] - Large savedInstanceState bundles: Saving adapter data or bitmaps triggers
TransactionTooLargeException. Fix: Store only IDs in bundles; useViewModelorSavedStateHandlefor large state. - Ignoring background crash rate: Crashes in Service/BroadcastReceiver/WorkManager still count toward Play vitals. Fix: Wrap background entry points in try/catch with Crashlytics logging. [src4]
- Not testing on low-memory devices: OOM crashes often appear only on devices with 2-3GB RAM. Fix: Use emulator with
-memory 2048. [src5] - Using deprecated AsyncTask: Deprecated in API 30 with well-known lifecycle leak issues. Fix: Replace with
kotlinx.coroutines. [src2] - Bitmap.Config.ARGB_8888 for all images: Uses 4 bytes/pixel when many images only need 2. Fix: Use
RGB_565for opaque images (50% memory savings). [src5]
Diagnostic Commands
# Capture crash logcat in real time
adb logcat *:E
# Filter for fatal exceptions only
adb logcat | grep -E "FATAL EXCEPTION|Process.*PID"
# Check ANR traces
adb shell ls -la /data/anr/
adb pull /data/anr/traces.txt ./anr_traces.txt
# Dump current process memory stats
adb shell dumpsys meminfo com.example.app
# Check memory limits on device
adb shell getprop dalvik.vm.heapsize
adb shell getprop dalvik.vm.heapgrowthlimit
# Monitor GC activity in real time
adb logcat -s "art" | grep -i "gc"
# Get application exit reasons (Android 11+)
adb shell dumpsys activity exit-info com.example.app
# Symbolicate native crash from tombstone
ndk-stack -sym path/to/obj/local/arm64-v8a/ -dump tombstone_00
# List tombstone files from native crashes
adb shell ls -la /data/tombstones/
# Capture a bug report with full system state
adb bugreport > bugreport.zip
Version History & Compatibility
| Android Version | Status | Crash-Related Changes | Migration Notes |
|---|---|---|---|
| Android 15 (API 35) | Preview | Stricter background process limits; tombstone format improvements | Test background services under new restrictions |
| Android 14 (API 34) | Current | Foreground service type required; stricter implicit intents | Add foregroundServiceType to manifest |
| Android 13 (API 33) | Supported | Runtime notification permission; per-app language | Add POST_NOTIFICATIONS permission |
| Android 12 (API 31) | Supported | Tombstone collection via Crashlytics NDK; strict exported flag | Set android:exported on all intent-filter components |
| Android 11 (API 30) | Supported | ApplicationExitInfo API; AsyncTask deprecated | Use getHistoricalProcessExitReasons() |
| Android 10 (API 29) | Maintenance | Scoped storage; background activity launch restrictions | Update file access; use PendingIntent for background launches |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Debugging production crash reports from Crashlytics/Play Console | Crash is a known SDK bug with a pending fix | Wait for SDK update; add workaround |
| Setting up crash prevention patterns in a new project | Issue is a build error or compilation failure | Standard build error debugging |
| Profiling ANR rates during performance optimization | Issue is slow rendering (jank) without ANR | Android GPU Inspector or Frame Profiler |
| Investigating OOM on low-memory devices | Memory is high but no actual crash occurs | Memory Profiler for optimization |
| Training team on crash-free coding practices | Crash only occurs in unit tests | Check test framework and mocking setup |
Important Caveats
- Crash-free rate thresholds differ between Google Play (1.09%) and internal quality bars — most top apps target < 0.5%.
- Pure Kotlin apps see ~30% fewer NPEs than mixed Java/Kotlin codebases (per Google Home team migration data).
- ANR trace files (
/data/anr/traces.txt) may be overwritten by subsequent ANRs — pull them immediately. - Native crash debugging requires matching debug symbols for the exact build — mismatched symbols produce wrong line numbers.
TransactionTooLargeExceptionis only thrown on Android 7.0+; earlier versions silently fail.