What actually causes an ANR on Android, and how do you prevent one?
An ANR isn't a crash — it's the system timing out because your main thread stopped responding, and fixing it starts with finding which timeout fired.
LEARN BY DOINGPut your intuition to work3 steps
What actually causes an ANR on Android, and how do you prevent one?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.An ANR — Application Not Responding — is not a crash. Nothing throws. The app is technically alive and its process is fine; the operating system has simply decided that your main thread has stopped answering within a bounded window, and it steps in to offer the user a "wait / close app" dialog. Knowing the exact mechanism matters in an interview, because "make it faster" is not an answer — the real question is "what makes Android give up on you, and after how long."
The timeouts are fixed and worth knowing cold: roughly 5 seconds for input dispatch (a key press or touch event that never gets acknowledged), about 10 seconds for a foreground BroadcastReceiver.onReceive() to return (60 seconds if the receiver runs in the background), and around 20 seconds for a foreground service to finish its startup callbacks. Different subsystems watch different clocks, but they all watch the same thing: whether the main thread ever got back to them.
That "same thing" is the real root cause. Android's UI thread runs a single Looper pulling messages off one MessageQueue — touch and key events, Activity and Fragment lifecycle callbacks, Choreographer frame callbacks, and most Binder callbacks arriving from system_server all funnel through that one thread, one message at a time. Block that thread for long enough and you don't just delay a frame — you delay the very message that would let some other subsystem know you're still alive. This is why "don't do slow things on the main thread" is practically the entire discipline of Android performance, and an ANR is the most dramatic possible violation of it.
In practice, the causes cluster into a short list: synchronous network calls or disk I/O triggered directly from a click handler; SharedPreferences.Editor.commit() (which blocks for the write) used where apply() would do; an unindexed SQLite query scanning a large table on the calling thread; a BroadcastReceiver.onReceive() that does real work instead of dispatching it elsewhere; and — the one that surprises people — a deadlock where a background thread holds a lock and is waiting on the main thread to release something, while the main thread is blocked waiting for that same lock. That last one produces an ANR with a main-thread stack trace that looks empty at first glance, because the thread isn't doing expensive work — it's just stuck.
For catching these before they ship, StrictMode.ThreadPolicy in debug builds will log (or crash, if you're strict about it) the moment disk or network I/O happens on the main thread. In production, Android 11 and later expose ApplicationExitInfo, which lets your own app query why its previous process was killed — including a full ANR trace via getTraceInputStream() — without needing adb or a rooted device to pull /data/anr/traces.txt. Play Console's Android vitals track your ANR rate against a threshold Google considers "bad behavior," which is the number that actually gets a team's attention.
Prevention is mostly about discipline, not clever tricks: push I/O onto Dispatchers.IO from a coroutine scope tied to a lifecycle, defer non-urgent background work to WorkManager instead of doing it inline, keep onReceive() down to dispatching work elsewhere (or use goAsync() with an explicit deadline if you truly need a few more seconds), and audit any code that takes more than one lock for consistent ordering — inconsistent lock ordering is exactly how the main thread ends up waiting on a lock it can't get in time.
Below is the classic mistake and the fix. The first version blocks the main thread with a synchronous database call in a click listener, capable of blowing the 5-second input-dispatch timeout on a slow device or a big table. The second version moves the same work behind a coroutine, keeping the main thread free to keep dispatching input while the query runs.
// BAD: runs on the main thread — a big table or a slow device// can blow past the 5s input-dispatch timeout.button.setOnClickListener {val orders = database.orderDao().loadAllForUserBlocking(userId) // synchronousrenderOrders(orders)}
// BETTER: main thread only dispatches; the query happens off it,// and the UI update is posted back automatically by the dispatcher switch.class OrdersViewModel(private val dao: OrderDao) : ViewModel() {private val _orders = MutableLiveData<List<Order>>()val orders: LiveData<List<Order>> = _ordersfun loadOrders(userId: String) {viewModelScope.launch {val result = withContext(Dispatchers.IO) {dao.loadAllForUserBlocking(userId)}_orders.value = result // back on Main automatically}}}
A senior answer doesn't stop at "move it off the main thread," though. It also separates the three failure modes that all look like an ANR from the outside — a genuinely slow synchronous call, a deadlock (main thread blocked on a lock, no CPU being burned), and a long GC pause caused by allocation churn (main thread blocked, but by the runtime, not your code) — because the fix and the trace look different for each one, and misreading a lock wait as "just slow code" is a common way to ship a fix that doesn't fix anything.
🧠 ANRs: check your understanding
0/3 · 0/3 answered1. An ANR is triggered when...
2. Why does a deadlock between a background thread and the main thread often show an ANR trace where the main thread appears idle, not busy?
3. What's the main practical advantage of ApplicationExitInfo over pulling /data/anr/traces.txt manually?