Exceptional Exceptions for Coroutines made easy…?

Anton Spaans
Jul 1 · 12 min read
“Kernel Panic” by Kevin Collins.

What is this blog post really about?

We started building our app, we chose Kotlin and we decided to use Coroutines as our main pattern to deal with asynchronous code. No regrets! And this article is not meant to convince us of this, or otherwise.

When are Exceptions thrown or re-thrown?

For a regular function

An exception is thrown by simply calling the throw operator.

throw IllegalStateException("name cannot be empty")
fun doSomething() {
...
throw Exception(msg)
...
}
fun String.celciusToFahrenheit(): String {
val c = this.toInt() // toInt may cause an exception
return (32.0 + (c.toDouble() / 5.0) * 9.0).toString()
}

For a suspend function

An exception is re-thrown by a suspend function when (1) the code in its body throws an exception, (2) the code does not catch a received exception from another regular or suspend function or (3) when one of its child-Coroutines finishes with an uncaught exception [see Structured Concurrency].

suspend fun doSomethingRemote() {
...
throw Exception(msg)
...
}
suspend fun remoteTempInFahrenheit(): String {
val c = server
.getCelcius()// suspend fun may cause an exception
.toInt() // regular fun may cause an exception
return (32.0 + (c.toDouble() / 5.0) * 9.0).toString()
}
suspend fun throwsExceptionBecauseOfAsyncChild() {
coroutineScope {
// This 'async' will cause an exception
// with or without(!) calling 'child.await()',
// because 'child' finishes with an exception.
val child = async { throw SomeSillyException() }
}
...
}
suspend fun throwsExceptionBecauseOfLaunchChild() {
coroutineScope {
// This 'launch' will cause an exception,
// because 'child' finishes with an exception.
val child = launch { throw SomeSillyException() }
}
...
}

For a Coroutine

An exception finishes a Coroutine when (1) its code throws an exception, (2) its code does not catch a received exception from another regular or suspend function or (3) when one of its child-Coroutines finishes with an uncaught exception [see Structured Concurrency].

scope.launch {
...
throw Exception(msg)
...
}
val remoteTempInFahrenheit: Deferred<String> = scope.async {
val c = server
.getCelcius()// suspend fun may finish this coroutine
.toInt() // regular fun may finish this coroutine
return (32.0 + (c.toDouble() / 5.0) * 9.0).toString()
}
scope.launch {
// This 'async' will finish this coroutine
// with or without(!) calling 'child.await()',
// because 'child' finishes with an exception.
val child = async { throw SomeSillyException() }
...
}
scope.async {
// This 'launch' will cause finish this coroutine,
// because 'child' finishes with an exception.
val child = launch { throw SomeSillyException() }
...
}

Summary

Both regular functions and suspend functions re-throw any uncaught exception that was received in the body of those functions. In addition, a suspend function also re-throws any exception that was not caught by any child-Coroutine launched in its scope.

How are Exceptions handled?

We visited a few scenarios in which exceptions can be thrown and re-thrown. Let’s see how our code could handle them.

For a regular function

There’s nothing new here, we’re very familiar with regular (and blocking) functions. There are two options:

fun parseNumber(input: String): Int = 
try {
input.parseInt()
} catch (e: Throwable) {
-1
}

For a suspend function

The designers of Kotlin’s Coroutines went to great lengths to make sure that there are as few differences as possible between writing a regular function and writing a suspend function. This also is true when we need to write code that handles exceptions thrown by the code inside a suspend function.

suspend fun Network.parseNumber(url: URL): Int =
try {
this.getData(url).parseInt()
} catch (e: NumberFormatException) {
-1
}
suspend fun Network.parseNumber(url: URL): Int =
suspendCancellableCoroutine { cont ->
this.requestData(url) {
try {
cont.resume(it.parseInt())
} catch (e: NumberFormatException) {
cont.resume(-1)
} catch (e: Exception) {
cont.resumeWithException(e)
}
}
}

For a Coroutine

As with the other scenarios, we can just catch exceptions locally by using a try & catch clause.

val deferredResult: Deferred<Int> = scope.async {
try {
network.getData(url).parseInt()
} catch (e: NumberFormatException) {
-1
}
}

Finishing a Coroutine with an exception

When a Coroutine, built by calling CoroutineScope.async or CoroutineScope.launch, finishes with an (uncaught) exception, its parent-Coroutine will be finished with that exception, as long as both of them were built within the same CoroutineScope. This keeps going up the child-parent Coroutine chain until we reach a Coroutine that has no parent-Coroutine (that was built within the same scope). This is called the top-most Coroutine.

val deferredResult = async {
// This child-Coroutine throws an exception
throw SomeSillyException()
}
textView.rating = try {
deferredResult.await()
} catch (e: SomeSillyException) {
// We catch the exception and assign a default value
-1
}
// At this point, this parent-Coroutine has finished with
// 'SomeSillyException' as well, even though we caught
// the exception just now!

For a top-most Coroutine created from a call to “async”

Say the top-most async-Coroutine finishes with an exception. We still can handle the exception, albeit in a different scope (since it was the top-most Coroutine)

val deferredResult = scope1.async {
throw SomeSillyException()
}
scope2.launch {
textView.rating = try {
deferredResult.await()
} catch (e: SomeSillyException) {
-1
}
}

For a top-most Coroutine created from a call to “launch”

Any result returned by a launch-Coroutine is ignored. A launch is a fire-and-forget type of Coroutine.

val scope = CoroutineScope()scope.launch {
throw SomeSillyException()
}
// Will cause the UEH to be invoked.
// It will most likely print a stack-trace to the error-log
// and crash our app.
val errorHandler = CoroutineExceptionHandler { context, error ->
when (error) {
is SomeSillyExeption -> ...
...
}
}
val scope = CoroutineScope(errorHandler)scope.launch {
throw SomeSillyException()
}
// Will cause the 'errorHandler' to be invoked
// and our app won't crash.
val scope = CoroutineScope(errorHandler)scope.launch {
throw SomeSillyException()
}
// Will cause the 'errorHandler' to be invoked.
val scope = CoroutineScope()scope.launch(errorHandler) {
throw SomeSillyException()
}
// Will also cause the 'errorHandler' to be invoked.
val scope = CoroutineScope()scope.launch {
launch(errorHandler) {
throw SomeSillyException()
}
}
// Will cause the UEH to be invoked instead and our app may crash,
// because the 'errorHandler' is not installed in the
// scope of the top-most Coroutine.

For a new CoroutineScope

A newly created CoroutineScope allows the launching of the so-called top-most Coroutines that will run concurrently with the code that builds them, by calling CoroutineScope.launch or CoroutineScope.async. A CoroutineScope contains a Job that will be the parent-Job of these Coroutines.

val scope = CoroutineScope(Job())val child1 = scope.async {
...
throw SomeSillyException()
}
val child2 = scope.launch {
... this Coroutine may be cancelled ...
}
... a while later, after SomeSillyException was thrown ...
val child3 = scope.launch {
... this Coroutine will never start ...
}

CancellationException

OK…? “CancellationException.” What about it?

  • If the Coroutine is a top-most launch-Coroutine, it will not send the CancellationException to the CEH nor to the UEH.

Code Examples

Below are a few code examples that show how exceptions are thrown and handled.

Recap

Throwing and catching exceptions when dealing with suspend functions is largely the same as how we deal with them using regular functions. We can use try & catch clauses and exceptions are thrown up a Coroutine Call Stack as they are thrown up a Thread Call Stack.

One more thing… SupervisorJob

Coroutines in this blog post are run with a Job, which allows the finishing of a Coroutine to finish its parent-Coroutine. When a Coroutine is run with a so-called SuperviserJob, this does not happen. Stay tuned for more information about this in a soon-to-follow blog post.

The Kotlin Chronicle

The Kotlin Chronicle is a publication which covers all things related to the Kotlin programming language and the community around it.

Anton Spaans

Written by

Principal Software Engineer at @intpd, part of @Accenture. You can find me online @streetsofboston or at https://www.linkedin.com/in/antonspaans/

The Kotlin Chronicle

The Kotlin Chronicle is a publication which covers all things related to the Kotlin programming language and the community around it.