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
- Part 1: how exceptions work for JVM and Android apps
- Part 2: implementing an UncaughtExceptionHandler in a JVM app
- Part 3: sending crash reports to an error reporting API
- Part 4: capturing non-fatal Android errors
- Part 5: handling obfuscation and minification in Android crash reports
- Part 6: adding useful metadata to crash reports
- Part 7: capturing signals and exceptions in Android NDK apps