Coroutines and exceptions: things to know
Coroutines are an awesome way to write async code. Your code looks almost similar to the sync equivalent but it's not blocking.
When everything goes according to plan, no need to worry about weird behavior from coroutines. What happens though when things don't go according to plan, i.e. exceptions are thrown? Can those exceptions be caught like regular sync code?
The answer depends on the coroutine builder used.
async
The async
coroutine builder works as you would expect, i.e. similar to how sync code handles exceptions. They rely on the user to handle the exception, otherwise, it's thrown as unhandled.
val deferred = GlobalScope.async {
throw ArithmeticException()
}
try {
deferred.await()
} catch (e: ArithmeticException) {
// Exception caught
}
launch
On the other hand, launch
coroutine builder treats exceptions as unhandled. These can be caught by Java's Thread.uncaughtExceptionHandler
.
try {
GlobalScope.launch {
throw ArithmeticException()
}
} catch (e: ArithmeticException) {
// Exception will *not* be caught
}
Of course, relying on Thread.uncaughtExceptionHandler
is not a great idea to handle all exceptions across your app.
Coroutine builders accept an additional CoroutineExceptionHandler
parameter for these cases.
val handler = CoroutineExceptionHandler { _, exception ->
// Exception caught
}
GlobalScope.launch(handler) {
throw ArithmeticException()
}
Child-parent exception relationship
Speaking of exceptions, we should note the default behavior of coroutines when they encounter an exception.
A child coroutine encountering an exception will cancel itself and its parent coroutine with that exception.
val handler = CoroutineExceptionHandler { _, exception ->
// ArithmeticException will be caught.
//
// 1st child will not complete because parent is cancelled (i.e. and
// children as well)
}
GlobalScope.launch(handler) {
launch { // 1st child
delay(Long.MAX_VALUE)
print("This will not be printed")
}
launch { // 2nd child
throw ArithmeticException()
}
}
A small detail, but the original exception will be handled by the parent after all its children coroutines terminate. So, if the 2nd child was running with NonCancellable
context, the handler would be called after the completion of the 2nd child.
val handler = CoroutineExceptionHandler { _, exception ->
// 1st child complete, then ArithmeticException will be caught.
}
GlobalScope.launch(handler) {
launch(NonCancellable) { // 1st child
delay(Long.MAX_VALUE)
print("This will be printed")
}
launch { // 2nd child
throw ArithmeticException()
}
}
A special exception
The CancellationException
is treated differently than the rest of the exceptions. It's ignored by the exception handlers and does not cause the cancellation of the parent coroutine.
launch {
val child = launch {
delay(Long.MAX_VALUE)
}
child.join()
child.cancel() // Parent is not cancelled,
// although child is throwing a CancellationExeption
print("Hello from parent")
}
Multiple exceptions
In case multiple children throw exceptions, the first one wins. There is a way to catch additional exceptions that might have been thrown after the first one.
val handler = CoroutineExceptionHandler { _, exception ->
// ArithmeticException will be caught (from 2nd child).
//
// IOException can be accessed using exception.suppressed (from 1st child)
}
GlobalScope.launch(handler) {
launch { // 1st child
try {
delay(Long.MAX_VALUE)
}
finally {
throw IOException()
}
}
launch { // 2nd child
throw ArithmeticException()
}
}
Hopefully, the relationship between coroutines and exception is a bit clearer now. You can always refer to the excellent Kotlin docs. Happy throwing!