Skip to content
Android App Crashes: Causes, Diagnosis, and Fixes

16 Minutes

Android App Crashes: Causes, Diagnosis, and Fixes

Fix Bugs Faster! Log Collection Made Easy

Get started

Android app crashes are a nightmare for developers. But they rarely come out of the blue. Most crashes stem from structural causes like leaky memory or sloppy resource management, and most failures will have a root cause that surfaces in either logcat or the stack trace.

This post will give you a true Android crash course, showing you the tell-tale signs and the tried-and-tested fixes that work across devices, territories and app utilities. You’ll learn about:

  • The most common Android crash types and what triggers each one.
  • How to read logcat output and stack traces to pinpoint the cause.
  • Concrete fixes for each crash type, with the right tools and APIs.
  • How to catch crashes in production before users report them.

Quick flag: If you’re a user looking to fix apps crashing on your Android phone rather than a developer, this post isn’t for you. Try this guide instead.

What are the consequences of an Android app crash?

You probably know that App crashes will invariably lead to high subscription churn and a state of uninstalls. But they can also directly affect your app’s position on Google Play.

Two particularly important thresholds to bear in mind here:

  • 1.09% of overall daily active users experiencing a user-perceived crash.
  • 8% of daily active users on a single device model.

Exceed either of these and your ranking may drop, or you may trigger a warning on your store listing.

What causes an Android app to crash?

Android app crashes fall into six common failure types. Each one triggers either an unhandled exception or a signal the app cannot recover from.

Crash typeWhat it signals
NullPointerExceptionAccessing an object reference that holds no value
OutOfMemoryErrorInsufficient heap memory to allocate resources
ANRMain thread blocked for more than 5 seconds
IllegalStateExceptionOperation called at the wrong lifecycle stage
Native crash (SIGSEGV, SIGABRT)Crash in C/C++ or NDK layer
NetworkOnMainThreadExceptionNetwork call executed on the main thread

Now let’s look at each of the failure types in detail.

NullPointerException in Android apps

Android App Crashes - NullPointerException

NullPointerExceptions have long been the most common cause of Android app crashes. Historically they have accounted for up to 40% of failures.

The cause is simple: code tries to call a method or access a property on an object reference that is null.

How to recognize it in logcat

A NullPointerException that crashes the app always surfaces as a FATAL EXCEPTION in the crash log. You can read it in several places:

  • Android Studio Logcat (View > Tool Windows > Logcat). Filter by ‘Fatal’ or your package name during local development.
  • ADB in the terminal. Run adb logcat with no IDE needed, useful in CI environments.
  • Play Console. Crash stack traces appear directly in Android vitals for production crashes.
  • Crash reporting tools. Tools like Bugfender (yep, that’s our house product) show the stack trace in a dashboard with additional session context.

Regardless of where you read it, the trace will look like this:

FATAL EXCEPTION: main
java.lang.NullPointerException: Attempt to invoke virtual method...
    at com.example.app.MainActivity.onCreate(MainActivity.java:42)

The at line gives you the exact file and line number where the null reference was accessed.

How to fix NullPointerExceptions in Kotlin

In Kotlin, the type system handles most null safety at compile time. When dealing with nullable values, the right approach depends on whether null represents a bug or an expected state.

Sometimes a null is unexpected and unacceptable. Other times, such as when a user has not uploaded a photo or a product description is not available, we have to ride it by either skipping, acknowledging or offering a workaround.

Unexpected NullPointer exceptions

If null should never occur,requireNotNull() / !! will make the problem visible immediately instead of allowing invalid state to propagate.

val user = requireNotNull(getUser()) { "User must not be null at checkout" }

Expected NullPointer exceptions

Kotlin’s safe-call operator (?.) is a good option if you want the app to simply skip over the problem and move on.

user?.sendNotification() // skips silently if user is null

If you want to acknowledge the issue gracefully, user-facing fallback lets you redirect the user to a fallback action, rather than simply crashing.

val user = getUser()
if (user == null) {
    showError("Please log in to continue") // shown to the user
    return
}

Alternatively, you can use ?: (Elvis operator) to provide a fallback value. For example if the user has not provide a name, you can default to ‘Guest.’

val username = user?.name ?: "Guest"

How to fix NullPointerExceptions in Java

Java was designed in the 1990s before null-related bugs were widely regarded as a language-design problem. Nullability is not enforced by the type system, so you need to add it explicitly by:

  • Annotating method parameters and return values with @NonNull and @Nullable (Jetpack annotations) so the compiler surfaces warnings before the crash reaches a device.
  • Validating values coming from external sources like intents, bundles, or API responses before calling methods on them.
String name = intent.getStringExtra("username");
if (name != null) {
    displayName(name);
}

For a broader look at how Kotlin handles exceptions, our Kotlin exception handling guide covers the patterns in detail.

OutOfMemoryError in Android apps

Heap memory stores the data that grows or shrinks at runtime, like objects and arrays. OutOfMemoryError crashes happen when the app requests more heap memory than the system can provide.

These crashes are most common on lower-end devices and when handling large bitmaps, images, or unclosed resources.

How to detect memory leaks

LeakCanary documentation page showing two Android phones with a list of detected memory leaks and a detailed leak trace.

Add LeakCanary to your debug build dependencies. It’s a memory leak detection library for Android and it automatically detects retained objects, displaying a readable report in the device notification tray.

You won’t need any additional code or IDE, and you won’t need to perform any manual heap analysis. LeakCanary installs itself and starts watching for leaks automatically.

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'

For real-time heap monitoring, use whichever tool fits your setup:

  • Android Studio or IntelliJ: Memory Profiler shows heap allocation over a session visually.
  • Any terminal or AI-assisted environment: run adb shell dumpsys meminfo com.example.myapp (replacing com.example.myapp with your app’s applicationId from build.gradle) and watch the Heap Alloc value across multiple runs.

If heap size grows steadily without dropping after a garbage collection cycle, you have a leak.

How to fix memory and resource issues

Load images at display size, not full resolution. A single unresized bitmap can exhaust memory on low-end devices.

Glide.with(context)
    .load(imageUrl)
    .override(targetWidth, targetHeight)
    .centerCrop()
    .into(imageView)

Close resources promptly. Leaked cursors, streams, sockets, or native resources can exhaust system resources and contribute to memory problems. Kotlin’s use block handles this automatically.

context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
    while (cursor.moveToNext()) {
        // process rows
    }
} // cursor closed automatically

Avoid holding strong references to Context inside long-lived objects like singletons or ViewModels. Use the application context instead.

class MyRepository(private val appContext: Context) {
    // safe: application context lives as long as the app
}

For a deeper look at the most common patterns, our Android memory leaks guide covers each one with fixes.

ANR errors in Android apps

Android system dialog stating “Android Date Widget isn’t responding,” with options to close the app or wait.

An Application Not Responding (ANR) occurs when the app’s main thread or a critical component becomes unresponsive for longer than Android’s timeout for that operation (often around 5 seconds for user input). These are often caused by network calls, database queries, file reads, or heavy computation running on the main/UI thread.

The user will see a system dialog asking whether to wait or force-close the app. Obviously that’s far from ideal. Exceeding 0.47% of daily active users triggers a bad behavior threshold that can affect your Play Store ranking and discoverability.

So we need to keep this computation off the main thread.

How to identify an ANR

ANRs leave traces at every stage of development. You’ve just got to look in the right place.

  • Logcat (any terminal or AI-assisted environment). Run adb logcat and look for ANR in com.example.myapp. The log includes the reason and the thread that was blocked.
  • adb bugreport. Generates a full bug report including ANR traces saved in /data/anr/traces.txt, useful for reproducing issues from a specific device.
  • StrictMode. Can detect many main-thread disk and network operations during development.
  • Play Console (production). Navigate to Monitor and improve > Android vitals > Crashes and ANRs. ANRs are grouped into clusters by root cause. Use the ‘Issue visibility’ filter and select ‘Foreground’ to see the ones that users actually experience.

How to fix ANR errors

The best way to fix errors is to prevent them from happening in the first place. And enabling StrictMode during development will help you stop thread blockages proactively.

StrictMode establishes your thread policy, and logs any violations to logcat. They’ll be visible in Android Studio, IntelliJ, or any terminal running adb logcat.

if (BuildConfig.DEBUG) {
    StrictMode.setThreadPolicy(
        StrictMode.ThreadPolicy.Builder()
            .detectAll()
            .penaltyLog()
            .build()
    )
}

Once you know what’s blocking the main thread, move it to a background dispatcher:

viewModelScope.launch(Dispatchers.IO) {
    val result = database.getUserData()
    withContext(Dispatchers.Main) {
        updateUi(result)
    }
}

It’s also worth noting some of the most common ANR triggers to watch for.

  • SharedPreferences reads on first launch. Migrate to DataStore, which is async by design.
  • Synchronous network calls. Always use suspend functions or callbacks, never blocking calls on the main thread.
  • Mutex locks across threads. Avoid locking the main thread while waiting for a background operation to complete.

And importantly: if you’re building with an AI coding tool and the generated code makes direct database or network calls without coroutines or threading, fix this before you do anything else.

IllegalStateException in Android apps

IllegalStateException crashes occur when a method is called at a point in the app lifecycle where that operation is not valid. The most common scenarios:

  • A network callback returns after the Activity has already stopped and tries to commit a fragment transaction.
  • show() is called on a DialogFragment after the host Activity has already called onSaveInstanceState().
  • A ViewModel is accessed after it has been cleared and its lifecycle has ended.

In all cases the lifecycle is invalid by the time the code runs, and Android throws rather than risk an inconsistent UI state.

How to find an IllegalStateException

In logcat, look for:

  1. The exception type java.lang.IllegalStateException.
  2. A message like Can not perform this action after onSaveInstanceState, telling you which operation was invalid.
  3. The stack trace line pointing to the exact transaction or operation that fired too late.

How to fix lifecycle-related crashes

As with ANR crashes, the best fix is the one you don’t have to make. Adopt proactive prevention behaviors and you’ll minimize the risk of problems later.

Never perform UI operations from a callback without checking the lifecycle state first. If you are using fragments, check the state before committing any transaction:

if (!isStateSaved && isAdded) {
    parentFragmentManager.beginTransaction()
        .replace(R.id.container, newFragment)
        .commit()
}

If you are using StateFlow or LiveData, use lifecycle-aware collection so updates stop automatically when the observer is stopped or destroyed:

viewModel.uiState
    .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
    .onEach { state -> updateUi(state) }
    .launchIn(lifecycleScope)

If you are using Jetpack Compose, collect state with collectAsStateWithLifecycle(), which handles lifecycle awareness automatically:

val uiState by viewModel.uiState.collectAsStateWithLifecycle()

For a broader look at how Kotlin handles exceptions in these scenarios, our Kotlin exception handling guide covers the patterns in detail.

Native crashes in Android (SIGSEGV, SIGABRT)

We’ve covered the crashes you’re most likely to encounter in Kotlin and Java. However, Android apps can also include native code written in C or C++, accessed through the Android Native Development Kit (NDK). These native components introduce their own class of crashes.

C and C++ crashes produce a tombstone file rather than a Java stack trace, which makes them harder to read without the right setup.

  • SIGSEGV signals a segmentation fault, typically a null pointer dereference or out-of-bounds memory access in native code.
  • SIGABRT usually means an explicit abort() call triggered by an assertion failure or detected heap corruption.

How to read a native crash stack trace

One of the really distinctive features of raw native crash traces is that they show memory addresses instead of function names:

signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
backtrace:
  #00 pc 0000000000042f89  /data/app/com.example/lib/arm64/libexample.so

To get readable symbols, upload a native debug symbols file to Play Console under Release > App bundle explorer > Downloads. Play Console deobfuscates the trace automatically.

Alternatively, run ndk-stack locally against the tombstone file, pointing it to the folder containing your .so files for the target ABI:

adb logcat | ndk-stack -sym path/to/your/obj/local/arm64-v8a

How to fix native crashes

Enable AddressSanitizer (ASan) in your NDK debug build to catch memory errors at the exact point of occurrence rather than when they cascade into a crash.

How do you enable it? That depends on your build system:

For CMake, add to CMakeLists.txt:

target_compile_options(mylib PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(mylib PRIVATE -fsanitize=address)

For ndk-build, add to Application.mk:

APP_CFLAGS := -fsanitize=address -fno-omit-frame-pointer
APP_LDFLAGS := -fsanitize=address

Add logging before suspected failure points to build a breadcrumb trail in logcat:

#include <android/log.h>
__android_log_print(ANDROID_LOG_DEBUG, "MyLib", "Entering processData, ptr=%p", ptr);

Validate all pointers at JNI boundaries before dereferencing. Treat every value crossing from Java to native as potentially null:

JNIEXPORT void JNICALL Java_com_example_MyClass_process(JNIEnv *env, jobject obj, jlong ptr) {
    if (ptr == 0) {
        __android_log_print(ANDROID_LOG_ERROR, "MyLib", "Null pointer received in process()");
        return;
    }
    // safe to proceed
}

For deeper NDK debugging workflows, our ADB debugging guide covers the toolchain in detail.

NetworkOnMainThreadException in Android apps

NetworkOnMainThreadException is thrown when code attempts a network operation directly on the main thread. This is a common problem when the developer is new to Android and places network code inside a UI callback, or when older synchronous networking code is copied into a modern Android app.

On API level 11 and above, Android throws NetworkOnMainThreadException when a network operation is performed on the main thread.

How to detect it

In logcat, filter by your package name and look for:

android.os.NetworkOnMainThreadException

The stack trace points directly to the call site. This crash surfaces immediately in debug builds. Even when this specific exception is not encountered, blocking work on the main thread can lead to ANRs.

How to fix threading errors

The fix is always the same: move the network call off the main thread. How you do it depends on your stack:

Kotlin with coroutines:

viewModelScope.launch(Dispatchers.IO) {
    val response = apiService.fetchUser(userId)
    withContext(Dispatchers.Main) {
        updateUi(response)
    }
}

Kotlin with Retrofit: declare the service function as a suspend function and Retrofit handles thread switching automatically:

interface ApiService {
    @GET("users/{id}")
    suspend fun fetchUser(@Path("id") userId: String): User
}

Java: use a background thread explicitly with ExecutorService:

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
    Response response = apiService.fetchUser(userId);
    runOnUiThread(() -> updateUi(response));
});

Ktor or other async clients: these are async by design. Make sure you are calling them from within a coroutine scope, not from a direct UI callback or onCreate.

Important: Never disable StrictMode to silence the error. It is a detection mechanism, not the problem itself.

A few final tips to prevent Android app crashes

Before we move on to production, here are some final process-level habits that have served us well at Bugfender. We’ve found they catch most crashes before users get hit:

PracticeWhat it catches
Run lint and static analysis in your CI pipelineNull safety violations, threading issues, deprecated API usage before a single device is involved
Test on a low-end device before every releaseOutOfMemoryError and ANR crashes that only appear under real memory and CPU constraints
Test on slow or throttled networksNetworkOnMainThreadException patterns and ANRs triggered by slow API responses
Review AI-generated code for null checks, threading, and lifecycle awarenessAI tools often skip these; the generated logic may be correct but the safety patterns are frequently missing
Use staged rollouts on Play ConsoleLimits blast radius if a new crash slips through; roll back before it reaches your full user base

How to monitor Android crashes in production

We’ve covered all the obvious issues during development. Stay on top of these cases and you’ll go a long way to preventing Android app crashes.

However, a significant share of production crashes only appear on specific device models, OS versions or network conditions that are impossible to replicate locally.

Android vitals in Play Console

The Play Console aggregates crash data from devices where users have opted into diagnostics sharing. Crashes are grouped into clusters by stack trace similarity, so you can prioritize by the number of affected users rather than raw crash count.

For a full breakdown of how Android vitals works and what each metric means, our Android vitals guide covers it in detail.

Remote logging with Bugfender

Play Console shows you what crashed. It does not show you what the user was doing in the 30 seconds before it happened.

We built Bugfender to close that gap. It captures a continuous log of device activity and sends it to our dashboard when a connection is available, even if the app crashes before the log is flushed out.

When a crash report comes in, you open the session timeline and read exactly what happened: which screens were visited, which API calls were made, what state the app was in. That context is what cuts debugging time.

If you also develop for iOS, we have separate guides on reading iOS crash reports and iOS crash debugging.

Try Bugfender free, setup takes under 5 minutes.

Key takeaways: Fix Android crashes faster with the right context

Every Android crash has a traceable root cause. The six crash types covered in this guide account for the large majority of what you will encounter in production.

The pattern is the same for all of them: read the stack trace, identify the failure point, apply the right fix for your stack, and put monitoring in place so the next one surfaces before a user reports it.

If your app has issues that don’t produce a crash at all, our guide on non-crashing bugs covers those patterns.

Frequently asked questions about Android apps crashes

What is the difference between a crash and an ANR?

A crash terminates the app process immediately due to an unhandled exception or signal.

An ANR occurs when the app is still running but the main thread is unresponsive for too long.

Crashes are easier to detect in logcat; ANRs are often harder to reproduce because they depend on timing, device load, and background conditions. Both affect your Android vitals score in Play Console.

Why does my app crash only on specific devices?

Device-specific crashes usually point to memory constraints on lower-end hardware, manufacturer-level Android customizations that alter system behavior, or GPU and driver differences that affect rendering.

In Play Console, use the per-device filter in Android vitals to identify the affected model, then configure an Android Virtual Device in Android Studio with matching specs to reproduce it locally.

Does crash rate affect Play Store visibility?

Yes. Google treats user-perceived crash rate as a core Android vital. Apps exceeding 1.09% of daily active users experiencing a crash across all devices, or 8% on a single device model, are flagged and may rank lower or display a warning on their store listing.

How do I get crash logs from users in production?

Play Console provides aggregated crash clusters from opted-in devices. For session-level detail, a remote logging tool like Bugfender captures the full device log leading up to a crash, including custom log statements, network calls, and UI events, and makes it searchable in a dashboard without requiring any action from the user.

Expect The Unexpected!

Debug Faster With Bugfender

Start for Free
blog author

Aleix Ventayol

Aleix Ventayol is CEO and co-founder of Bugfender, with 20 years' experience building apps and solutions for clients like AVG, Qustodio, Primavera Sound and Levi's. As a former CTO and full-stack developer, Aleix is passionate about building tools that solve the real problems of app development and help teams build better software.

Join thousands of developers
and start fixing bugs faster than ever.