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 [CODE]UncaughtExceptionHandler[/CODE] 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 [CODE]UncaughtExceptionHandler[/CODE], and setting it as the default handler for all exceptions in the JVM. We'll implement the [CODE]UncaughtExceptionHandler[/CODE] interface, then call [CODE]Thread.setDefaultUncaughtExceptionHandler[/CODE] to override the JVM's default implementation:
-- CODE language-kotlin --
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, [CODE]SimpleExceptionHandler[/CODE] 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 [CODE]Throwable[/CODE] 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 [CODE]Report[/CODE] class which can hold many arbitrary fields:
-- CODE language-kotlin --
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 [CODE]UncaughtExceptionHandler[/CODE] will now generate a [CODE]Report[/CODE] object that contains a stacktrace for each unhandled error. We'll also call [CODE]Thread.getAllStackTraces()[/CODE] 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 [CODE]Foo[/CODE], 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 [CODE]Delivery[/CODE] interface that delivers a Report to an arbitrary location:
-- CODE language-kotlin --
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 [CODE]Delivery[/CODE], 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.