mobilegame-qacrashesanrcrashlytics

Crashes and ANRs in a mobile game: catching what won't reproduce for you

The worst part of crashes isn’t the crash. The worst part is the phrase “it doesn’t reproduce for me.” You stare at a stack trace from production, try to repeat it, and nothing. Because crashes almost never live on your device. They live on someone else’s: where memory is tight, the network drops, the OS is old, ten apps are in the background and the battery is at three percent.

What follows is about catching what won’t fall over for you. And, more importantly, about first making sure you’re even getting the reports at all. Because in my experience the trouble is usually not that crashes “don’t get caught,” but that the reporter is set up so half of them never reach you — while the team is sure “Crashlytics is in place.”

First, test the reporter itself

It sounds obvious, but this is exactly where it usually leaks. Let me show it on a real example — I recently went through a startup log of a game on iOS.

Here’s the picture. Crashlytics loads — and at that very moment the log shows FirebaseCore errors: No app has been configured yet and Add FirebaseApp.configure(). So Crashlytics is being touched before Firebase was even configured. Then a test exception arrives — thrown on purpose to check whether a crash is captured. And a second later a line pops up in the log: the uncaught-exception handler (NSUncaughtExceptionHandler) belongs to GADRegisterExceptionHandler — meaning it was grabbed by the ad SDK (Google Mobile Ads), not Crashlytics.

Put it together. The test crash happened before Crashlytics had come up, and the exception handler was owned by a foreign SDK. A crash like that most likely just never reached the console. And that’s the first bug QA should catch: not the crash itself, but the fact that the crash wasn’t reported.

What to check in the reporter itself:

  • Init order: Firebase/Crashlytics comes up before everything else, and before the ad and analytics SDKs that also love to install their own exception handler.
  • A deliberate test crash shows up in the console — Firebase has a ready way to verify this (see test your implementation). Remember: the report usually goes out not at crash time but on the next launch — check after a restart.
  • Who ends up owning the uncaught handler: if several SDKs fight over it, one loses and some crashes vanish.

Until you’re sure a test crash gets through, chasing real ones is pointless. Fix the pipe first, then look for what’s flowing through it. Basic setup is in the Crashlytics guide.

A crash and an ANR are different things

A crash — the app fell over. An ANR (Application Not Responding) — it’s alive but frozen: the main thread is blocked for more than about five seconds and the system offers to “close.” For the user an ANR is often worse than a crash. A crash is fast and unambiguous; a freeze is when you tap and tap and nothing happens — the “it’s broken” feeling is stronger.

And an ANR is caught differently — not via an exception but by monitoring the main thread (on Android you see it in Android Vitals → ANR). The cause is almost always the same: heavy work on the main thread — network, disk, database, parsing a big JSON. In games, add loading assets right in the frame and long operations in the main loop. If yours “freezes for a couple of seconds sometimes,” that’s not minor — it’s a future ANR in the store’s stats.

A stack without symbols is garbage

A separate pain is an unreadable report. If you see bare addresses instead of function names, the symbols aren’t loaded: dSYM on iOS, mapping/symbols on Android, and Unity’s own symbol files. Make sure they’re uploaded in CI on every build. Otherwise the situation is silly: you have crashes, but where they are is unknown. On reading iOS crashes — Apple on crash reports; on Android — Android Vitals.

A bare stack isn’t enough — you need context

A single stack often won’t tell you what happened. What actually fills in the picture: breadcrumbs (the last screens and actions before the crash), custom keys (level, balance, online/offline, A/B group and feature flags), version, build, device, OS, free memory. In a game, “crashed on level 37 offline with almost no memory” is already a hypothesis, while “crashed” is not.

In Unity there are two kinds of crash

If the game is on Unity, remember crashes come in two flavors: managed (a C# exception) and native (a crash of the engine or a native plugin). They’re captured and symbolicated differently, and it’s easy to end up with C# exceptions arriving neatly while native crashes slip past. Make sure both are caught (see Crashlytics for Unity).

How to reproduce what won’t fall over for you

Now back to that phrase, “it doesn’t reproduce for me.” Production crashes hinge on conditions that don’t exist on your desk — so you create those conditions deliberately instead of waiting for luck:

  • Squeeze memory — fill the device with apps or enable a test low-memory mode: you catch OOM kills and out-of-memory crashes.
  • Give it a bad, dropping network — timeouts and mid-request drops expose unhandled errors.
  • Kill the app in the background and come back — the system unloaded the process while the code expects the state to be there.
  • Spam taps and jab buttons fast — double-taps and races.
  • Run on an old OS and weak hardware from your fragmentation matrix, not just the flagship device.

OOM kills, races on returning from the background, and double-taps are, in my experience, the most common sources of what “doesn’t reproduce” for the tester but reliably crashes a slice of real users.

In short

Crashes aren’t about waiting for something to fall over. They’re about two things. First, reliably getting the reports — and that’s a separate thing you have to test, as we saw with the foreign exception handler and the under-initialized Crashlytics. Second, recreating other people’s conditions rather than hoping to repeat it on your own perfect device. “It doesn’t reproduce for me” isn’t a conclusion — it’s where the work starts.

Sources: Firebase Crashlytics — Get started, Crashlytics — Test your implementation, Android Vitals — ANR, Android Vitals — Crashes, Apple — Crash reports.