Kotlin coroutine context and exception handling

what is the context

CoroutineContext is a set of elements used to define coroutine behavior, including the following parts:

  • Job: Control the life cycle of the coroutine
  • CoroutineDispatcher: distribute tasks to appropriate threads
  • CoroutineName: The name of the coroutine, very useful when debugging
  • CoroutineExceptionHandler: Handle uncaught exceptions
  • These parts can be combined by “+”

@Test
fun `test coroutine context`() = runBlocking {
    launch(Dispatchers.IO + CoroutineName("test")) {
        println("thread: ${Thread.currentThread().name}")
    }
}
Inheritance of coroutine context
  • For a newly created coroutine, its CoroutineContext will contain a brand new Job instance, which will help us control the life cycle of the coroutine.
  • The remaining elements will be inherited from the parent class of CoroutineContext, which may be another coroutine or the CoroutineScope that created the coroutine.

Coroutine context = default value + inherited CoroutineContext + parameters

  • Some elements contain default values: Dispatchers.Default is the default CoroutineDispatcher, and “coroutine” is the default CoroutineName
  • The inherited CoroutineContext is CoroutineScope or the CoroutineContext of its parent coroutine
  • Parameters passed into the coroutine builder have higher priority than inherited context parameters, so the corresponding parameter values ​​will be overwritten.

@Test
fun `test coroutine context extend`() = runBlocking {
    val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> 
        println("handle exception: $throwable")
    }
    //scope有了新的CoroutineContext,和runBlocking不一样
    val scope = CoroutineScope(Job() + Dispatchers.Main + coroutineExceptionHandler)
    //job的CoroutineContext继承自scope,但是Job会是新的,每个协程都会有新的Job
    val job = scope.launch(Dispatchers.IO) { 
        //新协程
    }
}

Since the parameters passed into the coroutine builder have a higher priority, the job’s scheduler is overridden and is Dispatchers.IO instead of the parent class’s Dispatchers.Main.

abnormal

abnormal spread

Coroutine builders have 2 forms of propagation:

  • Automatically propagate exceptions (launch and actor), expose exceptions to users (async and produce)
  • When these builders are used to create a root coroutine (the coroutine is not a sub-coroutine of another coroutine), the former will be thrown as soon as a builder exception occurs, while the latter relies on the user for final consumption. Exception, for example by calling await or receive
  • Exceptions generated by non-root coroutines are always propagated
Characteristics of exception propagation

When a coroutine fails due to an exception, it propagates the exception to its parent. Next, the parent will perform the following steps:

  • Cancel its own child coroutine
  • cancel itself
  • Propagate and pass the exception to its parent
SupervisorJob and SupervisorScope
  • When using SupervisorJob, the failure of one sub-coroutine will not affect other sub-coroutines. SupervisorJob will not propagate exceptions to its parent, it will let the sub-coroutines handle the exceptions themselves.
  • Or for sub-coroutines in SupervisorScope, if one fails, other sub-coroutines will not be affected. However, if there is an exception failure in the coroutine scope, all sub-coroutines will fail and exit.

Exception catching

  • Use CoroutineExceptionHandler to capture coroutine exceptions
  • Timing: The exception is thrown by the coroutine that automatically throws the exception (when using launch instead of async)
  • Location: In the CoroutineContext of CoroutineScope or in a root coroutine (a direct subcoroutine of CoroutineScope or supervisorScope)
  • The handler must be installed in the external coroutine, not in the internal coroutine, otherwise the exception will not be caught.

@Test
fun `test exception handler`() = runBlocking {
    val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
        println("handle exception: $throwable")
    }
    val scope = CoroutineScope(Job())
    //能捕获到异常
    val job1 = scope.launch(coroutineExceptionHandler) {
        launch { 
            throw IllegalArgumentException()
        }
    }
    val job2 = scope.launch() {
        //不能捕获到异常
        launch(coroutineExceptionHandler) { 
            throw IllegalArgumentException()
        }
    }
}
Global exception handling in Android
  • The global exception handler can obtain uncaught exceptions that are not handled by all coroutines, but it cannot catch exceptions. Although it cannot prevent program crashes, the global exception handler is still very useful in scenarios such as program debugging and exception reporting.
  • We need to create the META-INF/services directory under the classpath and create a file named kontlinx.coroutines.CoroutineExceptionHandler in it. The content of the file is the full class name of our global exception handler.

class GlobeCoroutineExceptionHandler : CoroutineExceptionHandler {
    override val key = CoroutineExceptionHandler
    override fun handleException(context: CoroutineContext, exception: Throwable) {
        Log.d("xxx", "unHandle exception: $exception")
    }
}

Then create a new resources/META-INF/services directory in the main directory, and then create a new kontlinx.coroutines.CoroutineExceptionHandler file with the following content:

com.example.kotlincoroutine.GlobeCoroutineExceptionHandler
Cancellation and exception handling
  • Cancellation is closely related to exceptions. CancellationException is used internally in the coroutine to cancel exceptions, but this exception will be ignored.
  • When a child coroutine is canceled, its parent coroutine will not be canceled
  • If a coroutine encounters an exception other than CancellationException, it will use that exception to cancel its parent coroutine. When all child coroutines of the parent coroutine have ended, the exception will be handled by the parent coroutine.

//取消与异常
/*
* 打印顺序为:
* section 3
* section 1
* section 2
* handle exception:ArithmeticException
* 
* */
@Test
fun `test exception handler2`() = runBlocking {
    val handler = CoroutineExceptionHandler { _, throwable ->
        println("handle exception: $throwable")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE)
            }finally {
                withContext(NonCancellable){
                    println("section 1")
                    delay(100)
                    println("section 2")
                }
            }
        }
        launch {
            delay(10)
            println("section 3")
            throw ArithmeticException()
        }
    }
    job.join()
}
Exceptional aggregation
  • When multiple sub-coroutines of a coroutine fail due to exceptions, the first exception is generally handled. All other exceptions that occur after the first exception will be bound to the first exception.
  • Other exception information can be printed out through exception.suppressed.contentToString