Fix Common Errors

Fix Android ANRs: Unblock Your Main Thread in 5 Minutes

Android ANRs from main-thread blocking: root causes (I/O, expensive work, locks), how to diagnose in production, and threading solutions to fix them.

Android ANRs (Application Not Responding) appear when your main thread blocks for more than 5 seconds on a foreground app, or 15 seconds in the background. The system kills the app and shows users the crash dialog. If you're debugging Android ANRs caused by main-thread blocking, you're dealing with one of the most frustrating production issues: an app that seems fine in testing but freezes in users' hands. The root causes are often simple—I/O on the main thread, expensive calculations, or lock contention—but they're invisible without the right diagnosis.

This guide walks you through the main-thread blocking culprits, how to find them in production, and how to prevent them. You'll learn to use stack traces, threading tools, and error tracking to turn ANRs from mystery crashes into solvable problems. If you're already familiar with how to debug production errors, you know that most crashes have patterns—ANRs are no exception.

What Causes Android ANRs: The Main Thread Bottleneck

Android enforces a strict rule: keep the main thread free to render frames (60 fps = 16 ms per frame) and respond to user input. Any work that blocks the main thread for too long will trigger an ANR. The three common culprits are I/O, calculation, and lock contention.

I/O on the Main Thread

The most common cause is reading or writing data synchronously on the main thread. This includes network calls, database queries, file operations, and SharedPreferences access.

// WRONG: blocks the main thread
val user = db.userDao().getUser(userId)  // Database query blocks
textView.text = user.name

// CORRECT: off-load to a background thread
lifecycleScope.launch(Dispatchers.IO) {
    val user = db.userDao().getUser(userId)
    withContext(Dispatchers.Main) {
        textView.text = user.name
    }
}

Android's StrictMode will warn you about disk or network access on the main thread in development, but many apps still do it—especially in older codebases. In production, a slow network or disk can turn a 200 ms operation into a 7-second freeze.

SharedPreferences.getSharedPreferences() parses the entire XML file synchronously. If your preferences file grows large, every read can block for hundreds of milliseconds.

Long-Running Calculations

Sorting a large dataset, parsing JSON, or heavy cryptographic work on the main thread can block for seconds. The problem grows with device age and data size.

// WRONG: CPU-intensive work on main thread
val sorted = largeList.sortedByDescending { complexCalculation(it) }

// CORRECT: move to background thread
Thread {
    val sorted = largeList.sortedByDescending { complexCalculation(it) }
    runOnUiThread {
        adapter.submitList(sorted)
    }
}.start()

Modern code should use Dispatchers.Default for CPU-bound work, which distributes the load across thread pools.

Lock Contention

If your app synchronizes multiple threads around a shared resource, one thread may wait indefinitely for a lock held by another. For example, a background thread holding a lock while doing I/O can starve the main thread.

// WRONG: lock held while blocking on I/O
synchronized(lock) {
    val data = expensiveNetworkCall()  // Main thread waits here
}

// CORRECT: release the lock, then make the call
val data = expensiveNetworkCall()
synchronized(lock) {
    updateUI(data)
}

Diagnosing ANRs in Production

In the lab, use Android Studio's Profiler to watch thread activity and catch blocking calls. But production ANRs are harder: they depend on real networks, real data, and real devices. Your error tracker is your primary tool.

When an ANR occurs, Android generates an ANR exception in the crash log. The stack trace shows exactly which thread was blocked and what code was holding it. It's your map to the freezing code—similar to how Java NullPointerExceptions reveal the exact line that failed.

// Example ANR stack trace (simplified)
Thread: main
  at java.net.SocketInputStream.read(SocketInputStream.java:123)
  at okhttp3.internal.io.RealBufferedSource.read(RealBufferedSource.kt:234)
  at com.example.api.fetchUser(ApiClient.kt:45)
  at com.example.MainActivity.onCreate(MainActivity.kt:23)

This trace tells you: the main thread called fetchUser() on line 45, which tried to read from a network socket. The network was slow, and onCreate() never returned, so the ANR fired after 5 seconds.

Enable error-tracking-best-practices in your app: capture ANRs automatically, tag them with device model and network type, and group them by stack trace. This turns a flood of crash reports into actionable patterns.

Prevention: Move Work Off the Main Thread

The antidote is threading. Android gives you several tools:

  • Coroutines (Kotlin): Use lifecycleScope.launch(Dispatchers.IO) for I/O and Dispatchers.Default for CPU work. Coroutines handle thread switching elegantly and integrate with lifecycle.
  • RxJava (Java): Use .subscribeOn(Schedulers.io()) and .observeOn(AndroidSchedulers.mainThread()) to move subscription to background and observe on main.
  • Threads (last resort): Hand-rolled threads are error-prone and hard to cancel; use them only if you have no choice.
  • WorkManager (long-running): For background tasks that outlive your Activity, use WorkManager to ensure they run safely and persist across reboots.

Audit your codebase for these patterns:

  1. Database queries in onCreate() or onResume()
  2. Network calls anywhere on the main thread
  3. Large list sorts or JSON parsing in adapters
  4. Synchronized blocks around I/O

Move them all off-thread.

Real-World Example: Fixing a Common ANR

Here's a typical bug: loading a user profile on app start.

// WRONG: MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    
    val user = db.userDao().getUser()  // ANR: database read on main thread
    userNameView.text = user.name
}

// CORRECT: use Coroutines
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    
    lifecycleScope.launch {
        val user = withContext(Dispatchers.IO) {
            db.userDao().getUser()
        }
        userNameView.text = user.name
    }
}

The second version moves the database read to the I/O dispatcher, freeing the main thread to render. The UI updates when the data arrives.

If your app must display cached data immediately, load it from memory (a fast in-process store), and refresh from disk/network in the background. This gives users instant feedback and prevents ANRs during the fetch.

Monitoring ANRs at Scale

Production ANRs are invisible until a user files a one-star review. Integrate ANR detection into your error tracker:

  • Automatic capture: The Sentry Android SDK automatically detects and reports ANRs. Point it at LightTrace by setting your DSN, and ANRs land in your dashboard with full stack traces and device context.
  • Grouping by root cause: ANRs with identical stack traces are grouped into a single issue. You'll see which ANR is crashing the most users and on which devices.
  • Reproduction clues: The breadcrumbs (taps, screen changes, network calls) leading up to the ANR help you reproduce it locally.

Example setup:

// build.gradle.kts
dependencies {
    implementation("io.sentry:sentry-android:7.+")
}

// AndroidManifest.xml
<meta-data
    android:name="io.sentry.dsn"
    android:value="https://<key>@light-trace.robomiri.com/1" />

// MainActivity.kt
import io.sentry.Sentry
Sentry.init { options ->
    options.attachStacktrace = true
    options.tracesSampleRate = 1.0
}

ANRs will now appear in your LightTrace dashboard alongside all other crashes. You'll see the main thread's stack trace, which threads were running, and the sequence of events leading to the freeze. From there, fix the I/O, move the calculation, or resolve the lock contention—and release a patch.

The Main Thread is Sacred

Every frame, every tap, every scroll depends on the main thread staying responsive. Blocking it for a few seconds feels like an app crash to your users. The cure is always the same: measure with a profiler, diagnose with stack traces from production, and move work off-thread using Coroutines or a similar threading tool. Build this habit early, and ANRs become rare.

Start tracking errors in minutes

Sign up for LightTrace free to capture ANRs automatically, group them by cause, and debug them with full context. Start free and monitor your Android app today.

Fix your next production error faster

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