2 Minutes: “Crash-free” doesn’t mean “works”

So you have 100% crash-free users. Does it mean your app works? Well, not all the time.

Check this out:

private void PotentialyFailingFunction(Object arg) {
if (arg == null) {
return;
}

//some logic here
//arg.toString(), arg.getClass()
}

Is the author cool? Not exactly. While he avoids NPE in this particular place (CRASHFREE!!!) his code is now in an unclear state. A wrapping code have called PotentialyFailingFunction() because a wrapping code expected this function to do something. But it just returns as soon as it sees null.

You can make it clearer using @NonNull:

private void PotentialyFailingFunction(@NonNull Object arg) {
if (arg == null) {
return;
}

//some logic here
//arg.toString(), arg.getClass()
}

We say “no nulls here please”. Problem is it’s not a runtime check, it’s still possible to crash if someone ignores Lint and sends null here, or null is run-time-generated. So we check it for null just in case, and the problem is still here — our app is not crashing but it’s also not working as well. And you do not know about it because reporting system says we are 100% crash-free.

It will not be crash-free in the real world — having this unclear state will generate the problem that will not be obviously connected to the original place most probably. It will crash 5 minutes later in a different module for a different reason. Now you have a nice debug.

Yeah, now you think about logging some data on a release build to see the state a minute before… Meh. Don’t log on a release. Don’t kill kittens.

Let it crash.

Let it crash in a graceful way. Let it give you all the information you might need to debug the app. You’ve already made it to the point where’s no turning back, no “ok, let’s try it a different way”, no second chance — app is diyng and you only can choose between simply dying and dying with all required information on hands.

  • Use your custom Exceptions or at least add custom descriptions — your logic is not only in your happy path, it’s in your Death Path as well.
  • Do not check for states you don’t know how to handle. If you do not have a clear idea of what to do next if x is null — do not check. It either should not be null at all (the best option) or you’re not familiar with your logic. Either way just checking for null is not a way to handle NPEs.
  • Keep multithreading in mind. Check the log — here one of almost 200 Observables is backpressured. Since no onError()’s were ever implemented… Well, it comes from Zygote, it goes up till it crashes the app and it doesn’t touch your code. Now you will need to implement error checks anyway. Always implement onError() in Rx! At least have some kind of a default error reporter to call if you do not know how to handle it — this way you will have the information.
Fatal Exception: java.lang.IllegalStateException: Exception thrown on Scheduler.Worker thread. Add `onError` handling.
 at rx.android.schedulers.LooperScheduler$ScheduledAction.run(LooperScheduler.java:112)

Caused by rx.exceptions.OnErrorNotImplementedException
 at rx.internal.util.InternalObservableUtils$ErrorNotImplementedAction.call(InternalObservableUtils.java:386)
 at rx.internal.util.InternalObservableUtils$ErrorNotImplementedAction.call(InternalObservableUtils.java:383)
 at rx.internal.util.ActionSubscriber.onError(ActionSubscriber.java:44)
 at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:153)
 at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:115)
 at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.checkTerminated(OperatorObserveOn.java:273)
 at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.call(OperatorObserveOn.java:216)
 at rx.android.schedulers.LooperScheduler$ScheduledAction.run(LooperScheduler.java:107)
 at android.os.Handler.handleCallback(Handler.java:605)
 at android.os.Handler.dispatchMessage(Handler.java:92)
 at android.os.Looper.loop(Looper.java:137)
 at android.app.ActivityThread.main(ActivityThread.java:4479)
 at java.lang.reflect.Method.invokeNative(Method.java)
 at java.lang.reflect.Method.invoke(Method.java:511)
 at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:825)
 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:592)
 at dalvik.system.NativeStart.main(NativeStart.java)
Caused by rx.exceptions.MissingBackpressureException
 at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.onNext(OperatorObserveOn.java:160)…
  • Every check you make is a logic. If you check something here, the next person might think you knew what you do while you didn’t. Code is not that clear now.
  • After all, think about your user. App have crashed — and he knows that there is a problem and “maybe if I do not press that button I can resolve my issue?”. App is behaving strange, ANRing, showing empty screens — and he’s not sure what’s happening, he’s frustrated, he do not know what to do next.

It’s not a goal to not crash. It’s a goal to make every crash as useful as possible.


2 Minutes are less-then-one-cigarette, while-brewing-coffee articles about different questions in Android. You can read other ones using tag 2 Minutes. I’m always happy to hear a feedback from you on how to make this thing better and what you’d like to read about.

Like what you read? Give Illia Kondratov a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.