Kotlin Training Program

DOWNLOAD APP

FEEDBACK

Exception Handling

During the execution of a coroutine, any exception / error might occur. Carefully handling exceptions is very important for a smooth user experience. Coroutine exception handling is different from general exception handling.

try-catch doesn’t work

We have already seen Exception handling using try-catch blocks, but they don’t seem to work with coroutines. Let’s see how. Observe the following program :

 fun main() {
    runBlocking {
        try {
            launch {
                error("Something went wrong!")
            }
        } catch (e: Exception) {
            println("ERROR : ${e.message}")
        }
    }
}

/* Output :

Exception in thread "main" java.lang.IllegalStateException: Something went wrong!
 */
 

Even though errorenous code is wrapped in try-catch block, the error goes unhandled, application is crashed and stacktrace is printed. Note that here launch() is wrapped in try-catch. Let’s try the other way around i.e. try-catch inside launch().

 fun main() {
    runBlocking {
        launch {
            try {
                error("Something went wrong!")
            } catch (e: Exception) {
                println("ERROR : ${e.message}")
            }
        }
    }
}

// Output : ERROR : Something went wrong!
 

This time the error was correctly handled by the try-catch block and the custom error message ERROR : Something went wrong! got printed.

Why is this so? Why is try-catch block unable to handle errors thrown by coroutines launched inside it? This is because launch() does not execute the input lambda directly. Based on the CoroutineContext & Dispatcher, the lambda execution is scheduled internally. So try-catch doesn’t work on the lambda passed. What’s the solution then? Well, one simple solution we just saw is to have try-catch inside launch(). But this isn’t reliable as it won’t be able to catch errors of child coroutines. Example :

 fun main() {
    runBlocking {

        // Main coroutine
        launch {
            try {
                // Child coroutine
                launch {
                    // Won't be caught
                    error("(Child) Something went wrong!")
                }

                delay(100)

                // Can be caught
                error("(Main) Something went wrong!")
            } catch (e: IllegalStateException) {
                println("ERROR : ${e.message}")
            }
        }
    }
}

/* Output :

ERROR : StandaloneCoroutine is cancelling
Exception in thread "main" java.lang.IllegalStateException: (Child) Something went wrong! */
 

Note :

To solve this problem, if we keep wrapping the code of each coroutine in try-catch blocks, our code will get messy. So, we won’t be using try-catch when dealing with exceptions inside coroutines. Instead, the best solution i.e. CoroutineExceptionHandler will be used.

CoroutineExceptionHandler

CoroutineExceptionHandler is a CoroutineContext Element. It is a class used to define the handler (lambda) to be executed for handling all exceptions occurring inside a CoroutineContext.

A CoroutineExceptionHandler can be created using the function of the same name and passing in the error handler lambda :

 fun CoroutineExceptionHandler(
		handler: (CoroutineContext, Throwable) -> Unit
): CoroutineExceptionHandler
 

We receive the CoroutineContext and the exception Throwable as input parameters to the lambda :

 val handler = CoroutineExceptionHandler { coroutineContext, throwable ->
    println("ERROR : ${throwable.message}")
}
 

The lambda passed here will be invoked for any exception that occurs in the CoroutineScope or coroutine where it is installed.

Handler can then be installed in Scope or coroutine builders :

 // Installed in runBlocking :
runBlocking(handler) { }

// Installed in custom CoroutineScope :
CoroutineScope(Dispatchers.Default + handler)

// Installed in coroutine using launch :
launch(handler) { }

// Installed in coroutine using async :
async(handler) { }
 

Note the use of plus operator + in CoroutineScope(Dispatchers.Default + handler) for combining multiple CoroutineContext elements like Dispatcher & CoroutineExceptionHandler.

Example (runBlocking)

Instead of try-catch block, let’s now use CoroutineExceptionHandler to catch a simple exception.

 import kotlinx.coroutines.*

val handler = CoroutineExceptionHandler { _, error ->
    println("ERROR : ${error.message}")
}

fun main() {

		// Handler won't work on runBlocking
    runBlocking(handler) {
        launch {
            error("Something went wrong!")
        }
    }
}

/* Output :

Exception in thread "main" java.lang.IllegalStateException: Something went wrong!
 */
 

Note that despite handler being installed on runBlocking scope, the exception isn’t caught, application crashes and stacktrace is printed.

⚠️ runBlocking does not support CoroutineExceptionHandler because runBlocking is a blocking function. Exception is thrown to the caller of runBlocking and not handled by the installed CoroutineExceptionHandler.

Example (Custom scope)

As CoroutineExceptionHandler does not work with runBlocking, so let’s install it in a custom CoroutineScope.

 val handler = CoroutineExceptionHandler { _, error ->
    println("ERROR : ${error.message}")
}

fun main() {
    runBlocking {
        CoroutineScope(handler).launch {
            error("Something went wrong!")
        }.join()
    }
}

// Output : "ERROR : Something went wrong!"
 

Note :

  • Exception thrown is now caught by the CoroutineExceptionHandler and neither application crashed nor stacktrace was printed.
  • handler is installed in custom scope built using CoroutineScope constructor.
  • The coroutine launched on custom scope is joined using join() function to await the coroutine completion because runBlocking waits only for its direct child coroutines and not other scope coroutines.

Supervisor scope

Need

Observe the following program & its output :

 val handler = CoroutineExceptionHandler { _, error ->
    println("ERROR : ${error.message}")
}

fun main() {

    runBlocking {

        CoroutineScope(handler).launch {

            // Job 1
            launch {
                println("J#1 start...")
                delay(200)
                println("J#1 end!")
            }

            // Job 2
            launch(handler) {
                println("J#2 start...")
                delay(100)
                error("J#2 failed!")
            }

            // Job 3
            launch {
                println("J#3 start...")
                delay(300)
                println("J#3 end!")
            }
        }.join()
    }
}

/* Output : 

J#1 start...
J#3 start...
J#2 start...
ERROR : J#2 failed!
 */
 

Note :

  • 3 coroutines are launched in parallel, where Job#2 fails after 100ms, when the other two jobs are yet to be finished.
  • On failure of Job#2, Job#1 & Job#3 also get cancelled. This is clear from the output where we see only start but no end print statements.

This program indicated that in a CoroutineScope, if a coroutine encounters an exception and fails, then all other sibling coroutines also get cancelled. This might lead to unexpected behaviour and inconsistencies when working with data. Suppose we send several network requests in parallel. If at least one of them fails, then all other requests’ coroutines will also get cancelled. To avoid this behaviour, we have SupervisorScope.

SupervisorScope is a special CoroutineScope such that its child coroutines can fail independent of each other.

The statement “fail independent of each other” means if one coroutine fails, it won’t affect the other coroutines i.e. they won’t be cancelled. Let’s see this in action.

Example (Basic)

As we saw earlier, SupervisorScope can be created using supervisorScope() suspend function. Let’s replace the custom scope from above discussed program with supervisorScope.

 fun main() {
    runBlocking {
        supervisorScope {
            
            // Job 1
            launch {
                println("J#1 start...")
                delay(200)
                println("J#1 end!")
            }

            // Job 2
            launch(handler) {
                println("J#2 start...")
                delay(100)
                error("J#2 failed!")
            }

            // Job 3
            launch {
                println("J#3 start...")
                delay(300)
                println("J#3 end!")
            }
        }
    }
}

/* Output :

J#1 start...
J#2 start...
J#3 start...
ERROR : J#2 failed!
J#1 end!
J#3 end! */
 

Note that even though J#2 failed, J#1 & J#3 were unaffected and finished normally. This is clear from the output. start and end both print statements of J#1 & J#3 can be seen.

This fail independently feature works only when coroutines are direct child of SupervisorScope. Following is an example where launch() coroutines are not direct child of SupervisorScope and they do not fail independently.

 fun basic1() {
    runBlocking {
        supervisorScope {
            launch(handler) {

                // Job 1
                launch {
                    println("J#1 start...")
                    delay(200)
                    println("J#1 end!")
                }

                // Job 2
                launch(handler) {
                    println("J#2 start...")
                    delay(100)
                    error("J#2 failed!")
                }

                // Job 3
                launch {
                    println("J#3 start...")
                    delay(300)
                    println("J#3 end!")
                }
            }
        }
    }
}

/* Output :

J#1 start...
J#2 start...
J#3 start...
ERROR : J#2 failed!
 */
 

It is clear from the output that J#1 and J#3 coudn’t finish due to failure of J#2 i.e. failure of J#2 affected other coroutines because they were not direct child of SupervisorScope.

👉 When using SupervisorScope, launch() coroutines that are direct child of it, fail independently of each other.

Here coroutines are launched using launch() function. Let’s now see how async() coroutines fail in SupervisorScope.

Example (Network requests)

Recall that async() function is used instead of launch() for launching coroutines that output some result. We shall now use async() function to send multiple network requests and compare custom scope and supervisorScope. We’ll use the number fact API to get fact about a specific number.

Firstly, let’s redefine the getMathFact() function such that it will throw an error in case request fails. We analyze the HttpStatusCode to check whether request failed or succeeded.

 private suspend fun getMathFact(num: String): String {
    println("GET MathFact($num)...")

    return client.get("http://numbersapi.com/$num/math").run {
        if (status == HttpStatusCode.OK) {
            bodyAsText()
        } else {
            error("Request failure!")
        }
    }
}
 

Next, we send multiple requests among which one is expected to fail intentionally due to invalid input. Notice that we have changed num input parameter of getMathFact() function, from type Int to String to allow invalid string inputs. In order to install CoroutineExceptionHandler, we use a custom scope.

 fun main() {
    runBlocking {
        CoroutineScope(handler).launch {
            listOf("1", "Q", "2").forEach {
                async { getMathFact(it) }
            }
        }.join()
    }
}
 

Here second request corresponding to input Q will fail while that of 1 & 2 will succeed. Notice that await() or awaitAll() function is not invoked i.e. results are not read, only requests are sent.

Following is the console output of the above program :

 GET MathFact(1)...
GET MathFact(Q)...
GET MathFact(2)...
ERROR : Request failure!
 

It shows that in case of exception in async() coroutine, the exception is delivered irrespective of await() or awaitAll() function being called. Here, we didn’t invoke await function and exception got delivered. It is clear from the ERROR : Request failure! line being printed. This is the case for all coroutine scope except SupervisorScope.

👉 Exception occurring in coroutines launched using async() function, are delivered irrespective of await function being invoked. SupervisorScope can be used to avoid this behaviour.

Let’s try using SupervisorScope now.

 fun main() {
    runBlocking {
        supervisorScope {
            listOf("1", "Q", "2").forEach {
                async { getMathFact(it) }
            }
        }
    }
}

/* Output :

GET MathFact(1)...
GET MathFact(Q)...
GET MathFact(2)...
 */
 

Note that when using SupervisorScope, **exception is not delivered when await function is not invoked.

However, this is not the case when async coroutines are not direct child of SupervisorScope.

 fun main() {
    runBlocking {
        supervisorScope {
            launch(handler) {
                listOf("Q", "1", "2").map {
                    async { getMathFact(it) }
                }
            }
        }
    }
}

/* Output :

GET MathFact(Q)...
GET MathFact(1)...
GET MathFact(2)...
ERROR : Request failure! 
 */
 

Here async() coroutines are child of launch() coroutine and not direct child of SupervisorScope. Even though await() is not invoked, exception is thrown and caught by handler.

👉 When using SupervisorScope and not invoking await function, exceptions of async() coroutines that are direct child of it, are not thrown.

Now let’s invoke the await() function to read the results.

 fun main() {
    runBlocking {
        supervisorScope {
            launch(handler) {

                listOf("1", "Q", "2")
                    .map { input ->
                        input to async { getMathFact(input) }
                    }
                    .forEach { (input, deferred) ->
                        println("Result($input) = ${deferred.await()}")
                    }
            }
        }
    }
}

/* Output :

GET MathFact(1)...
GET MathFact(Q)...
GET MathFact(2)...
Result(1) = 1 is the first figurate number of every kind, such as triangular number, pentagonal number and centered hexagonal number, to name just a few.
ERROR : Request failure!
 */
 

Note :

  • Output of 1st request (1) is delivered successfully but as soon as response of 2nd request (Q) is received, execption is thrown. This exception is delivered when invoking await() function and it is caught by the handler.
  • Third request (2) is also cancelled on failure of 2nd async() coroutine. Seems like even after using supervisorScope, async() coroutine’s failure is cancelling other coroutines. This is because on the main supervisorScope coroutine, await() function is throwing the coroutine exception halting the main coroutine. Thus, result of Third request (2) is not printed.

When using awaitAll(), no response is printed at all. Only the exception is delivered when awaitAll() is invoked.

 fun main() {
    runBlocking {
        supervisorScope {
            launch(handler) {

                val results = listOf("Q", "1", "2")
                    .map { async { getMathFact(it) } }
                    .awaitAll()

                println(results)
            }
        }
    }
}

/* Output :

GET MathFact(Q)...
GET MathFact(1)...
GET MathFact(2)...
ERROR : Request failure! */
 

We can fix this issue of not being able to read async() coroutines’ successful responses when one or more of them fail. For this, we have to wrap async() code in try-catch block.

 fun main() {
		runBlocking {
        listOf("1", "P", "2", "Q", "3")
            .map { input ->
                input to async {
                    try {
                        getMathFact(input)
                    } catch (e: Exception) {
                        e.message
                    }
                }
            }
            .forEach { (input, deferred) ->
                println("Result($input) = ${deferred.await()}")
            }
    }
}

/* Output :

GET MathFact(1)...
GET MathFact(P)...
GET MathFact(2)...
GET MathFact(Q)...
GET MathFact(3)...
Result(1) = 1 is also the first and second numbers in the Fibonacci sequence and is the first number in many other mathematical sequences.
Result(P) = Request failure!
Result(2) = 2 is a primorial, as well as its own factorial.
Result(Q) = Request failure!
Result(3) = 3 is the aliquot sum of 4.
 */
 

Note :

  • Now that we are using try-catch block to catch exceptions of async() coroutines, supervisorScope & handler is not required.
  • Failure of request P & Q did not affect other coroutines i.e. multiple async coroutines are launched that fail independent of each other.