Sobes.tech
Middle

What happens to a coroutine when an exception occurs?

sobes.tech AI

Answer from AI

When an exception occurs inside a coroutine, the dependency on the exception type (and dispatcher/context settings) determines how it will be handled.

  • Uncaught exception: If an exception is not caught inside a coroutine, it propagates up the coroutine hierarchy to the nearest parent Job (or CoroutineScope). The behavior depends on the type of Job:

    • Job: By default, if an exception occurs in a child coroutine, the parent Job is canceled, which leads to the cancellation of all its other child coroutines. The exception is rethrown upwards.
    • SupervisorJob: The parent SupervisorJob is not canceled when an exception occurs in a child coroutine. Only the coroutine where the exception occurred is canceled. This is useful when you want the cancellation of one child task not to affect others.
    • Root CoroutineScope: If the exception reaches the root CoroutineScope (for example, GlobalScope or a scope created with CoroutineScope without a Job), it is passed to the global exception handler CoroutineExceptionHandler. By default, if no handler is registered, the exception is logged to System.err.
  • Caught exception: If an exception is caught using a try-catch block inside a coroutine, it is handled locally, and the coroutine continues execution after the catch block, if possible. The exception is not propagated upwards.

  • Cancellation-related exceptions (e.g., CancellationException): These exceptions are special. They are used for coroutine cancellation mechanisms and are usually ignored by most exception handlers by default. They do not cause a fatal failure of the parent Job by default.

  • Exceptions in async: A coroutine launched via async places the exception in its Deferred. The exception is rethrown only when .await() is called on that Deferred. Until then, the exception is "hidden".

import kotlinx.coroutines.*

fun main() = runBlocking {
    val scope = CoroutineScope(SupervisorJob())

    scope.launch {
        try {
            println("Coroutine 1 started")
            delay(100)
            throw RuntimeException("Something went wrong in Coroutine 1")
        } catch (e: Exception) {
            println("Coroutine 1 caught exception: ${e.message}")
        }
    }

    scope.launch {
        println("Coroutine 2 started")
        delay(500)
        println("Coroutine 2 finished")
    }

    delay(1000) // Wait to see the output
    scope.cancel() // Cancel the scope
}

In this example, the exception in Coroutine 1 is caught, and Coroutine 2 continues execution because a SupervisorJob is used. If a regular Job were used instead, the exception in Coroutine 1 would lead to the cancellation of Coroutine 2.