Catching Bugs Early with StrictMode

AndroidDebuggingStrictModePerformance
Share on LinkedIn

StrictMode is the cheapest bug-prevention tool Android ships, and most teams either never enable it or enable it, get annoyed by the warnings, and turn it off. Both are mistakes. StrictMode catches an entire class of latent bugs — main-thread disk and network I/O, leaked Closeable resources, unclosed cursors, activity leaks — the moment they happen in development, long before they surface as an ANR on a user's slow device or a slow memory leak in the field. It's not a profiler you run occasionally; it's a tripwire you leave armed in every debug build.

The problem it solves

A SharedPreferences.getString or a small Room query on the main thread runs in under a millisecond on your flagship dev device. On a budget phone with a slow eMMC chip under memory pressure, that same call can block for 200ms — and if it happens during a touch event, that's a dropped frame or, chained with a few more, an ANR. You will never reproduce this locally by hand. StrictMode makes the mistake loud immediately: the first time your code touches disk on the main thread, it fires, regardless of how fast your device is.

Turning it on

Configure both policies in Application.onCreate, gated to debug builds:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()
                    .detectCustomSlowCalls()
                    .penaltyLog()
                    .penaltyFlashScreen()   // visible feedback on device
                    .build()
            )
            StrictMode.setVmPolicy(
                StrictMode.VmPolicy.Builder()
                    .detectLeakedClosableObjects()
                    .detectLeakedSqlLiteObjects()
                    .detectActivityLeaks()
                    .detectLeakedRegistrationObjects()
                    .penaltyLog()
                    .build()
            )
        }
    }
}

ThreadPolicy watches the current thread — in practice the main thread — for I/O and slow calls. VmPolicy watches the whole process for leaked resources. I start with penaltyLog() so every violation prints a full stack trace to Logcat pointing at the exact offending line.

penaltyDeath is a decision, not a default

The strongest signal is penaltyDeath(), which crashes the app on any violation. It sounds brutal, and it's exactly what you want on a fresh codebase or a specific policy you've already cleaned up — it makes regressions impossible to ignore because CI or a manual run dies instantly. But dropping penaltyDeath onto a legacy app that has hundreds of pre-existing violations just makes the app unusable and gets StrictMode disabled entirely. My approach:

The violations you'll actually see

In order of how often they catch real bugs:

Scoping short, legitimate work

Occasionally you have a genuinely necessary main-thread read that you've measured and accepted (rare, but it happens during a critical init). Rather than disable the detector globally, suppress it narrowly with permitDiskReads:

val old = StrictMode.allowThreadDiskReads()
try {
    // a small, measured, unavoidable read
    val theme = prefs.getString("theme", "system")
} finally {
    StrictMode.setThreadPolicy(old)
}

This keeps the policy strict everywhere else while documenting the one place you've consciously allowed a read. If you find yourself doing this a lot, that's a signal your architecture has too much synchronous work on the main thread — fix that, don't paper over it.

Getting production signal safely

The violations you hit locally are a subset of what real devices hit. On API 28+ you can attach a penaltyListener and report violations to analytics from internal or even production builds — without penaltyDeath, so nobody crashes:

StrictMode.ThreadPolicy.Builder()
    .detectAll()
    .penaltyListener(mainExecutor) { violation ->
        crashReporter.logNonFatal(violation)   // count, don't crash
    }
    .build()

I've caught real main-thread I/O this way that only manifested on specific OEM devices whose background services behaved differently than my test hardware. Treat those reports like the production jank signal from JankStats: a prioritized list of real-device problems you'd never see otherwise.

The habit that pays off

The teams whose apps stay responsive are the ones that leave StrictMode armed and treat a new violation as a build-breaking event, not a warning to scroll past. It costs nothing at runtime in release (it's off), it requires no ongoing effort once configured, and it converts an entire family of "works on my machine, ANRs in the field" bugs into a stack trace you fix in the minute you wrote them. Enable it, burn down the backlog, promote detectors to penaltyDeath, and never look back.

Resources

Frequently asked questions

What is StrictMode used for?

StrictMode is a developer tool that detects things your app does wrong that don't immediately crash — accidental disk or network I/O on the main thread, leaked Closeable resources, unclosed cursors, activity leaks, and untagged network sockets. It surfaces these during development so you fix them before they turn into ANRs or leaks in production.

Should StrictMode run in production?

Run it aggressively in debug builds, optionally with penaltyDeath to force fixes. In production you should not use penaltyDeath, but you can enable a lenient policy with penaltyLog or a custom penaltyListener to report violations to analytics, so you learn about real-device violations you never hit locally without crashing users.

What is the difference between ThreadPolicy and VmPolicy?

ThreadPolicy detects per-thread operations, primarily accidental disk and network I/O and slow calls on the main thread. VmPolicy detects process-wide problems like leaked Activities, leaked Closeable objects, unclosed SQLite cursors, and untagged sockets. You typically configure both in Application.onCreate for debug builds.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →