Error handling on Android part 4: capturing non-fatal Android errors

Jamie Lynch

Error handling on Android

Accent divider.

How many times have you been in the middle of using a new shiny app, only to have it crash on you?

Android app displaying a crash dialog

This is the fourth in a series of posts that will investigate how the exception handling mechanism works in Java and Android, and how crash reporting SDKs can capture diagnostic information, so that you're not flying blind in production.

Capturing other sources of Android errors

We previously learnt how to deliver crash reports to an error reporting API in a reliable manner, while gracefully handling conditions such as loss of network connectivity. Our goal now is to provide a way to capture non-fatal errors such as caught exceptions and ANRs, which usually represent a poor or unexpected user experience.

Why should we capture non-fatal errors?

Non-fatal errors can be as bad as crashes, and sometimes even worse, as your application could be in an unexpected state that confuses the user. A non-fatal error might be an exception that is caught within a try-catch block, or a performance issue such as an ANR, which indicates the main thread is blocked and the user might be about to kill your app. In the case of ANRs, a 3rd party library or bugsnag can detect when the main thread hasn’t processed a UI event in a long time, and then capture a stacktrace and deliver a report in the same way.

These errors won't be caught by an UncaughtExceptionHandler, meaning by default you won't have any observability that your users are experiencing frustrating events. Clearly we need some way of manually notifying a crash reporting service that an error occurred.

Writing a notify method

We'll achieve this by exposing a method on our crash reporting SDK that takes a #Throwable, and is publicly callable by other developers. If an exception is caught, then the developer can call #notify in the catch block:

try {
   TODO("Whoops!")
} catch (exc: Throwable) {
   notify(exc)
}

/**
* Notifies the crash reporter of a non-fatal error
*/
fun notify(throwable: Throwable) {
   createAndSendReport(throwable)
}

A great way of achieving observability without littering your codebase with a particular crash reporter implementation is to use Jake Wharton's Timber logging library. This library makes it really easy to install a custom Tree that automatically routes any logged #Throwables to a crash reporting service.

Recording application error state

Now that we've captured the non-fatal error, we want to distinguish between a handled and unhandled crash on a web app, so that developers can prioritise the most important bugs to fix. We can do this by recording whether or not the error was fatal, which in this simplified implementation below is achieved by passing a boolean:

class CrashReporter(val delivery: Delivery) : Thread.UncaughtExceptionHandler {
   override fun uncaughtException(thread: Thread, exc: Throwable) {
       createAndSendReport(exc, true)
   }

   fun notify(throwable: Throwable) {
       createAndSendReport(throwable, false)
   }

   private fun createAndSendReport(throwable: Throwable, fatalError: Boolean) {
       val report = Report(throwable, fatalError)
       delivery.deliver(report)
   }
}

In bugsnag, unhandled errors count towards your application's stability score, which allows you to determine whether you should spend time fixing bugs, or developing new features. It can also be helpful to capture additional information about the error at the point of capture, such as whether it was caused by an ANR, which is omitted in this simplified code snippet.

Capturing traces from RxJava and Kotlin Coroutines

A final thing worth noting is that sometimes libraries work in a way that means stacktraces from their uncaught exceptions are not particularly helpful. Libraries that employ concurrency such as RxJava and Kotlin Coroutines are two examples of this.

Fortunately, both libraries provide an API for capturing additional error information, in the form of RxJava's error handling operators, and coroutines' custom exception handler. These APIs make it possible to gain more context on a stacktrace than an #UncaughtExceptionHandler alone provides, giving developers more information to debug with if they perform a #notify call from within these handlers.

Using similar APIs it is possible to manipulate stacktraces entirely so that they can contain much more useful debugging information. One great example of a library that performs for this for RxJava is RxDogTag, which adds the inferred subscription point of RxJava observables to the stacktrace, which in effect points to the exact line where something went wrong.

In the next post, we'll learn how to construct a crash reporting service that handles stacktraces from an obfuscated app in production.

Would you like to know more?

Hopefully this has helped you learn a bit more about Error Handling on Android. If you have any questions or feedback, please feel free to get in touch.

———

Try Bugsnag's Android crash reporting.

Share