11 Minutes
Why do my apps keep crashing? 6 causes and how to fix them
Fix Bugs Faster! Log Collection Made Easy
An app crash is one of the fastest ways to drain both users and rankings. Many users will uninstall an app after just one crash, and both the App Store and Google Play will penalize crash-prone or unstable apps.
At Bugfender, we’ve been investigating app crashes since 2014. In this post we’re going to share all our key learnings, so you can:
- Identify the crash type before you even open a stack trace.
- Understand the 6 most common failure patterns on Android and iOS.
- Reproduce crashes reliably, even the ‘only in production’ ones.
- Set up remote logging to spot crashes before your users.
You’ll come away with a reliable, repeatable way to anticipate app crashes – wherever they happen.
This post is generally platform-agnostic. If you’re purely focused on one specific operating system, check out our specific fixes for both iOS and Android.
What is an app crash and why does it matter?
An app crash happens when a software application unexpectedly stops running and closes, or becomes unusable. This can create serious user retention problems, particularly as both iOS and Android demand flawlessness out of the gate.
- Google Play will actively demote apps that exceed its ‘bad behaviour’ thresholds.
- Apple says “apps that crash on launch will be removed immediately from the App Store” and it is generally believed (although not officially confirmed) that the App Store algorithm will reduce the visibility of crash-prone apps.
To achieve flawless performance, we need to do more than design great apps. We need to proactively anticipate crashes and reproduce them efficiently, no matter where they happen in our universe of downloads.
💡 Google now defines ‘bad behaviour’ as a 1.09% user-perceived crash rate or a 0.47% ANR (App Not Responding) rate.
Source: Android Vitals
Types of app crashes
There are four main types of app crash: a force close, a freeze/ANR, a background termination or a silent failure, where the app crashes without any obvious sign or message. Knowing which type you’re facing will enable you to optimize the response.
| Crash type | What it means |
|---|---|
| Force close | The OS terminates the process immediately. Usually an unhandled exception. The user is sent back to the home screen with no warning. |
| Freeze / ANR | The app is alive but the main thread is blocked. On Android, the system shows an ANR (Application Not Responding) dialog after 5 seconds. |
| Background termination | The OS kills the app silently to reclaim memory. Often mistaken for a crash by users. |
| Silent failure | The app keeps running but a critical operation fails without a visible error. The hardest type to catch without remote logging. |
Actually silent failures deserve a conversation all of their own.
If your app stays running but behaves incorrectly, that’s a different debugging problem entirely. Want the deep-dive? Check out our guide to non-crashing bugs.
Why apps keep crashing: the most common causes
Most crashes can be traced back to a predictable set of problems: code-level crashes (often linked to null references); resource-related crashes around inefficient memory usage and unnecessary retention; and compatibility issues.
Here’s a quick overview before we go deeper into each one.
| Cause | What triggers it |
|---|---|
| Memory leaks and OOM (Out-of-Memory) kills | The app holds onto objects it no longer needs until the OS terminates the process silently. |
| Unhandled exceptions and null errors | A null reference or uncaught exception terminates the process instantly. |
| Network and API failures | Requests fail without fallback handling, or API responses arrive in an unexpected format. |
| Incompatible OS or API versions | The app calls an API that doesn’t exist on the device’s OS version. |
| Device fragmentation | A layout, sensor, or hardware feature behaves differently across device models. |
| Thread and concurrency conflicts | A background thread touches the UI or writes to shared state without synchronization. |
1. Memory leaks and out-of-memory kills
A memory leak happens when code holds a reference to an object it no longer needs, so the runtime never frees that memory. It accumulates silently until the OS hits its limit and kills the process. There’s no error or warning. The app just closes.
Imagine this chain of events:
- We create a listener every time a screen opens but forget to remove it on close.
- Those listeners pile up in the background doing nothing.
- Open that screen 20 times, and 20 orphaned listeners are sitting in memory.
The fix is to always pair registration with cleanup:
| Android | iOS / Swift |
|---|---|
onResume() → onPause() | viewDidAppear() → viewDidDisappear() |
onCreate() → onDestroy() | viewDidLoad() → deinit |
2. Unhandled exceptions and null pointer errors
An unhandled exception crashes the app the moment your code tries to do something the runtime can’t allow: dividing by zero, accessing an index that doesn’t exist, or reading a property on an object that isn’t there.
The most common version is a null pointer error.
Imagine we’ve created a food delivery app. It fetches a saved address on launch but we can’t account for new users who haven’t added one yet:
- The API returns
nullfor the address field. - The code reads
address.streeton the next line. - On Android that’s a
NullPointerException. In Swift it’s a fatal unwrap error. - Every new user crashes on first open.
The fix is simple: carry out a single null check. These assumptions are invisible in development where test data is clean. Production is not.
3. Network connectivity and API failures
Most network failures come down to three principal patterns. A request times out and the app tries to render an empty response, the API changes a field from integer to string and the parser throws an exception, or the server returns a 500 and the app has no fallback UI.
Think of a news app.
- Typically, this app will load headlines on launch.
- But if the request times out and we try to display
nullas a string, the app crashes before the user sees anything. - A simple loading state and an error fallback would have kept it alive.
Network connectivity and API failures do not typically show up as edge cases in production, so be sure to validate all API responses before passing them to your data layer, and implement retry logic with exponential backoff for transient failures.
4. Incompatible OS or API versions
Every new OS release adds APIs and deprecates others. If we call a newer API without checking whether the device actually supports it, the app crashes silently on any device that hasn’t updated.
Think of a user still on an early Android version downloading our freshly updated app.
- If we added a feature introduced in a later version, their device has never heard of that method.
- The app calls that feature on launch.
- But Android 7 throws a
NoSuchMethodError. - The process terminates immediately and the user can’t open the app at all.
A single Build.VERSION.SDK_INT check on Android or an #available guard in Swift prevents it entirely.
5. Device fragmentation and hardware bugs
Android runs on thousands of device models. What works perfectly on a Pixel during development can crash on a budget phone, a foldable, or a device with a custom manufacturer skin.
Think of a fitness app that tracks movement with the gyroscope. If we don’t check whether the device actually has one before accessing it, the app crashes instantly on any device that doesn’t.
| Device variable | Crash scenario |
|---|---|
| Foldables | Layouts that work on fixed viewports throw exceptions on dynamic screen sizes |
| Missing sensors | Any feature that reads a gyroscope or barometer crashes if the device doesn’t have one |
| Custom camera HALs | Camera implementations that work on stock Android fail on Samsung or Xiaomi builds |
The fix is to always check for hardware availability before accessing it, and test on real device ranges, not just flagship simulators.
6. Thread and concurrency conflicts
Every Android and iOS app has a main thread responsible for rendering the UI. It has one rule: nothing else touches it. When a background operation violates that rule, the app crashes.
Think of a chat app that fetches new messages in the background. If we update the message count TextView directly from that background thread:
- On iOS, UIKit throws an exception immediately.
- On Android it sometimes works, which is worse: the bug hides in development and only surfaces under specific timing in production.
- The crash is intermittent and hard to reproduce without remote logging.
Remember to always dispatch UI updates to the main thread explicitly.
How to reproduce a crashed app reliably
Understanding the potential causes of crashes gives us a great headstart. But no amount of knowledge is sufficient unless we can actually reproduce crashes effectively. And this starts by isolating the trigger: the action, the data state, and the network condition that precedes it.
Use these steps to narrow it down:
- Check the last user action before the crash in your logs.
- Replicate the same data state in a test environment.
- Disable background processes to rule out concurrency as the trigger.
- Test on the same OS version and device model as the affected user.
If you can reproduce it consistently, you can fix it. But if you can’t, remote logging is the only reliable fallback. See our guide on non-crashing bugs for cases where the app keeps running but something has already gone wrong.
How to read crash logs and stack traces
A stack trace shows the sequence of method calls active at the moment the app crashed, from the most recent call at the top down to the origin at the bottom.
Remember to read from the top: the first line names the exception type and the file where it was thrown. The lines below show how execution reached that point.
| Stack trace element | What to look for |
|---|---|
| Exception type | Identifies the category of failure: null pointer, out of memory, illegal state |
| File and line number | Where in your code the crash occurred |
| Thread name | Whether the crash happened on the main/UI thread or a background worker |
A quick word of caution: third-party libraries often appear mid-stack. Look for your own package name to find where your code contributed to the failure.
How to prevent app crashes before they reach users
Many developers only catch crashes when they’ve reached production, which is already too late to prevent any fall-out. An effective error-catching regime will detect the problem earlier, ideally before a single user is affected.
There are two distinct ways you can accelerate detection:
- Automated testing regimes.
- Using remote crash reporting tools.
Many developers use both together, or borrow from both disciplines.
For iPhone and Android-specific fixes, see our guide on how to fix crashing apps on iPhone and Android.
Automated testing and CI pipelines
Unit tests catch exception-prone logic before it ships. UI tests cover the interaction paths most likely to bring the app down in the wild.
If we run both in a CI pipeline on every pull request:
- Null pointer errors get caught before they reach the branch.
- API parsing failures surface in the test suite, not in reviews.
- Thread violations trigger in the pipeline, not on a user’s device.
Remote crash reporting tools
Once an app is live and out in the world, you lose direct access. But remote crash reporting tools will send you the stack traces, device metadata and log trail, so you can keep track of your app across all devices and territories.
| What to look for | Why it matters |
|---|---|
| Remote log capture | See what happened before the crash, not just the exception |
| Device and OS metadata | Identify if the crash is device or version-specific |
| Real-time alerts | Catch a spike in crashes before it affects a large share of users |
At this point we’d like to talk briefly about our own remote logging technology, Bugfender. But if you’d like to see all the available options, check out our mobile crash reporting tools roundup.
Android crash logging with Bugfender
On Android, crashes that happen in production are invisible without remote logging. Logcat output stays on the device and disappears when the app is killed.
Bugfender captures crash reports remotely so you can read full stack traces from any device, without needing physical access or a USB cable. Setup takes only a few lines:
Bugfender.init(this, "YOUR_APP_KEY", BuildConfig.DEBUG)
Bugfender.enableCrashReporting()
Every uncaught exception is sent to the Bugfender dashboard with device metadata, OS version, and a full log trail leading up to the crash.
For a full walkthrough, see our Android crash debugging guide.
iOS and Swift crash logging with Bugfender
On iOS, Apple’s crash reporter captures symbolicated reports through Xcode Organizer, but only for TestFlight and App Store builds. Crashes in development or on beta devices outside TestFlight often go unlogged.
Bugfender fills this gap. Initialize it in your AppDelegate or @main entry point:
Bugfender.activate(withToken: "YOUR_APP_KEY")
Bugfender.enableCrashReporting()
You get the same remote crash visibility on iOS as on Android.
For deeper iOS-specific debugging, see our guides on iOS crash debugging and reading iOS crash reports.
FAQs about apps crashing
Why do my apps keep crashing after an OS update?
OS updates can deprecate APIs, change memory management behavior, or introduce stricter thread-safety rules. Apps that relied on undocumented behavior or called APIs at the edge of their support range are the most likely to break. Testing on OS betas before public release is the most reliable way to catch this early.
Why does one app crash but others work fine?
Each app runs in its own sandboxed process. A crash in one app doesn’t affect others. If a single app crashes consistently, the defect is in that app’s code, not in the OS or device hardware.
How do I stop an app from crashing in production?
Enable remote crash logging before release, not after. Once an app is live, you lose access to device logs. Tools like Bugfender send crash reports and log context remotely so you can diagnose and fix defects without waiting for user reports.
What is the difference between a crash and an ANR?
A crash gives you a stack trace pointing to the exact line of code that failed. An ANR means the main thread was blocked, no exception, but the system recorded which thread was stuck and what it was doing. Crashes point you to exception handling; ANRs point you to blocking operations on the main thread.
Expect The Unexpected!
Debug Faster With Bugfender