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 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.
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.
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.
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.
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.
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.