Error Handling on Android

420
Bugsnag
Bugsnag automatically detects errors in your mobile, front-end, and back-end applications. It gives your team the tools to fix things fast. 3,500 engineering teams such as Square, Shopify, and Airbnb use Bugsnag.

This post is by Jamie Lynch of Bugsnag


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

This is the first 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.

How do exceptions work for JVM and Android apps?

Exceptions should be thrown in exceptional circumstances where the calling code needs to decide how to recover from an error condition.

What is an Exception object?

A Throwable is a special type of object which can be thrown and alter the execution path of a JVM application. For example, the code snippet below throws an IllegalStateException:

fun main() {
   try {
       throw IllegalStateException()
       println("Hello World")
   } catch (exc: Throwable) {
       println("Something went wrong")
   }
}

Throwing an exception means that the execution flow changes, and 'Hello World' is never printed. Instead, the program counter will jump to the nearest catch block, and executes the error recovery code within, meaning our program prints ‘Something went wrong’ instead.

Of course, it doesn't always make sense to recover from a failure — for example, if an OutOfMemoryError is thrown by the JVM, there is very little prospect of ever recovering from this condition. In this case it makes sense to leave the Throwable as unhandled, and allow the process to terminate so the user can restart the app from a fresh state.

Anatomy of the Throwable class

Throwable has two direct subclasses: Exception, and Error. Typically an Error is thrown in conditions where recovery is not possible, and an Exception where recovery is possible. Additionally, there are many subclasses of Exception which convey additional meaning — for example, an IllegalArgumentException would indicate the programmer passed an invalid argument, and an IllegalStateException would indicate that the program encountered an unanticipated state.

fun main() {
   try {
       throw IllegalStateException("This should never happen!")
       println("Hello World")
   } catch (exc: Throwable) {
       println("Something went wrong")
   }
}

Let's consider the above snippet again. The constructed IllegalStateException object captures a snapshot of the application at the time of the error condition:

java.lang.IllegalStateException: This should never happen!
    at com.example.myapplication.Exceptions101Kt.foo(Exceptions101.kt:12)
    at com.example.myapplication.Exceptions101Kt.main(Exceptions101.kt:5)
    at com.example.myapplication.Exceptions101Kt.main(Exceptions101.kt)

This is commonly called a stacktrace. Each line represents a single frame in the application's call stack at the time of the error, which match the filename, method name, and line number of our original code snippet.

A stacktrace can also contain other useful information, such as program state, which in this case is a static error message, but we could equally pass in arbitrary variables.

Exception handling hierarchy

After throwing an exception, an exception handler must be found to handle the exception, or the app will terminate. In the JVM, this is a well-defined hierarchy, which we'll run through here.

First up in the exception handling hierarchy is a catch block:

try {
   crashyCode()
} catch (exc: IllegalStateException) {
   // handle throwables of type IllegalStateException
}

If a catch block isn't available in the current stack frame, but is defined further down the call stack, then the exception will be handled there.

Next in the hierarchy is implementations of UncaughtExceptionHandler. This interface contains a single method which is invoked whenever a Throwable is thrown, after the handler has been set:

val currentThread = Thread.currentThread()
currentThread.setUncaughtExceptionHandler { thread, exc ->
   // handle all uncaught JVM exceptions in the current Thread
}

It's possible to set an UncaughtExceptionHandler in a few different places; the JVM has a defined hierarchy for these. First, if a handler has been set on the current Thread, this will be invoked. Next up will be a handler on the ThreadGroup, before finally, the default handler is invoked, which will handle all uncaught JVM exceptions by printing a stacktrace, and then terminating the app.

Thread.setDefaultUncaughtExceptionHandler { thread, exc ->
   // handle all uncaught JVM exceptions
}

It's the default UncaughtExceptionHandler that is most interesting from an error reporting point-of-view, and it's the default UncaughtExceptionHandler that is responsible for showing that all too familiar crash dialog on Android.

The UncaughtExceptionHandler interface is the building block of all crash reporting SDKs on the JVM, such as bugsnag-android or bugsnag-java. Read on in part two to learn how we can define a custom handler for uncaught exceptions, and use it to create a crash reporting SDK.

Error handling on Android

Bugsnag
Bugsnag automatically detects errors in your mobile, front-end, and back-end applications. It gives your team the tools to fix things fast. 3,500 engineering teams such as Square, Shopify, and Airbnb use Bugsnag.
Tools mentioned in article