Error handling on Android part 2: implementing an UncaughtExceptionHandler in a JVM app

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
Android app displaying a crash dialog

This is the second 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 I implement a custom UncaughtExceptionHandler for a JVM app?

We previously learnt how an #UncaughtExceptionHandler allows us to handle uncaught exceptions in JVM applications. Our goal now is to create a simple handler that captures the stacktrace for every unhandled error, and generates a diagnostic report that could be sent to an error reporting API.

Implementing a basic UncaughtExceptionHandler

We'll start by implementing an #UncaughtExceptionHandler, and setting it as the default handler for all exceptions in the JVM. We'll implement the #UncaughtExceptionHandler interface, then call Thread.setDefaultUncaughtExceptionHandler to override the JVM's default implementation:

fun main() {
   val exceptionHandler = SimpleExceptionHandler()
   Thread.setDefaultUncaughtExceptionHandler(exceptionHandler)
   throw RuntimeException("Whoops!")
}

class SimpleExceptionHandler : Thread.UncaughtExceptionHandler {
   override fun uncaughtException(thread: Thread, exc: Throwable) {
       // TODO generate a diagnostic report
   }
}

Of course, #SimpleExceptionHandler isn't much use in its current form, as there isn't currently any handling code in our handler. Our next step will be to obtain a stacktrace from the #Throwable object, and gather other information to form a diagnostic report.

Capturing stacktrace information for error reports

Right off the bat we'll start off by encapsulating the stacktrace, so that we can capture additional metadata that may be useful in debugging our error. We'll do this by creating a #Report class which can hold many arbitrary fields:

fun main() {
   val exceptionHandler = SimpleExceptionHandler()
   Thread.setDefaultUncaughtExceptionHandler(exceptionHandler)
   throw RuntimeException("Whoops!")
}

override fun uncaughtException(thread: Thread, exc: Throwable) {
   val report = Report(exc)

In the example above, our #UncaughtExceptionHandler will now generate a #Report object that contains a stacktrace for each unhandled error. We'll also call Thread.getAllStackTraces() to obtain stacktraces for all running threads in our application, which can be immensely useful for tracking down those tricky concurrency bugs.

Finally, we'll add a field of type #Foo, to demonstrate that we can capture arbitrary information about the application at this point.

Delivering an error report on the JVM

After generating a basic error report, the next step in a crash reporting SDK would be to serialise the report to JSON. If all goes well, we'll then make a request to an error reporting API, so that we can quickly be altered that our app is crashing in production. We'll achieve this by adding a #Delivery interface that delivers a Report to an arbitrary location:

override fun uncaughtException(thread: Thread, exc: Throwable) {
   val report = Report(exc)
   delivery.deliver(report)
}

interface Delivery {
   fun deliver(report: Report)
}

There are a surprising amount of error conditions that we need to account for within the #Delivery, such as caching reports locally when there's no network connectivity, and ensuring the handler doesn't make long-lived requests, which can be killed by later versions of the OS. We'll cover this in more depth in our next post.

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