there are many factors can decide how exception and cancellation affect coroutine flows

i do not want to talk about too much theroy about it, let us feel it by examples

Example 01
job1 and job2 are all created from GlobalScope, they are independent coroutines
suspend fun main() {
    val dispatcher1 = CoroutineDispatchers.io()
    val dispatcher2 = CoroutineDispatchers.data()
    Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
        printWithThreadInfo(throwable.message + " " + "DefaultUncaughtExceptionHandler")
    }
    val errorHandler = CoroutineExceptionHandler { context, throwable ->
        printWithThreadInfo(throwable.message + " " + context[CoroutineName])
    }
    val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("1")) {
        val job2 = GlobalScope.launch(dispatcher2 + errorHandler + CoroutineName("2")) {
            delay(100)
            printWithThreadInfo("1")
            throw RuntimeException("crash")
        }
        delay(200)
        printWithThreadInfo("2")
    }
    delay(300)
    printWithThreadInfo("3")
    delay(999 * 1000L)
}
25 pool-2-thread-1 "1"
25 pool-2-thread-1 "crash CoroutineName(2)"
24 pool-3-thread-1 "2"
26 kotlinx.coroutines.DefaultExecutor "3"

conclusion

  • exception in job2 was caught by error handler in job2
  • exception in job2 only interrupt job2, not interrupt job1
Example 02
remove error handler for job2 this time
suspend fun main() {
    val dispatcher1 = CoroutineDispatchers.io()
    val dispatcher2 = CoroutineDispatchers.data()
    Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
        printWithThreadInfo(throwable.message + " " + "DefaultUncaughtExceptionHandler")
    }
    val errorHandler = CoroutineExceptionHandler { context, throwable ->
        printWithThreadInfo(throwable.message + " " + context[CoroutineName])
    }
    val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("1")) {
        val job2 = GlobalScope.launch(dispatcher2 + CoroutineName("2")) {
            delay(100)
            printWithThreadInfo("1")
            throw RuntimeException("crash")
        }
        delay(200)
        printWithThreadInfo("2")
    }
    delay(300)
    printWithThreadInfo("3")
    delay(999 * 1000L)
}
25 pool-2-thread-1 "1"
25 pool-2-thread-1 "crash DefaultUncaughtExceptionHandler"
24 pool-3-thread-1 "2"
26 kotlinx.coroutines.DefaultExecutor "3"

conclusion

  • if error handler is not set, exception will be handled by thread’s DefaultUncaughtExceptionHandler
  • job1 still won’t be interrupted by job2’s exception
UncaughtExceptionHandler

we just show one kind of UncaughtExceptionHandler above

actually, there are three kinds of UncaughtExceptionHandler, in order of priority, they are

  • thread’s uncaughtExceptionHandler, just available for current thread
  • global thread-default UncaughtExceptionHandler, available for all threads, if thread doesn’t have a self one
  • global coroutine UncaughtExceptionHandler, available for all coroutines, do nothing, just print error stacks
Example 03
cancel job1 this time, watch whether job2 will be cancelled next
suspend fun main() {
    val dispatcher1 = CoroutineDispatchers.io()
    val dispatcher2 = CoroutineDispatchers.data()
    val errorHandler = CoroutineExceptionHandler { context, throwable ->
        printWithThreadInfo(throwable.message + " " + context[CoroutineName])
    }
    val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("1")) {
        val job2 = GlobalScope.launch(dispatcher2 + errorHandler + CoroutineName("2")) {
            delay(300)
            printWithThreadInfo("1")
            throw RuntimeException("crash")
        }
        delay(300)
        printWithThreadInfo("2")
    }
    delay(100)
    job1.cancel()
    printWithThreadInfo("3")
    delay(999 * 1000L)
}
25 kotlinx.coroutines.DefaultExecutor "3"
24 pool-2-thread-1 "1"
24 pool-2-thread-1 "crash CoroutineName(2)"

conclusion

  • job2 won’t be cancelled, event if job1 is cancelled
  • job2 was created inside job1’s block, but they are in fact independent coroutines
Example 04
let job2 become job1’s child coroutine, then cancel job1, test what will happen
suspend fun main() {
    val dispatcher1 = CoroutineDispatchers.io()
    val dispatcher2 = CoroutineDispatchers.data()
    val errorHandler = CoroutineExceptionHandler { context, throwable ->
        printWithThreadInfo(throwable.message + " " + context[CoroutineName])
    }
    val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("1")) {
        val job2 = launch(dispatcher2 + errorHandler + CoroutineName("2")) {
            printWithThreadInfo("11")
            delay(400)
            printWithThreadInfo("12")
            throw RuntimeException("crash")
        }
        delay(200)
        printWithThreadInfo("2")
    }
    delay(100)
    job1.cancel()
    printWithThreadInfo("3")
    delay(999 * 1000L)
}
26 pool-2-thread-1 "11"
25 kotlinx.coroutines.DefaultExecutor "3"

conclusion

  • job2 started, but was cancelled during suspend period
  • job2 was launched by job1’s CoroutineScope, so it’s job1’s child coroutine
  • child coroutine will be cancelled, when parent job is cancelled
Example 05
let child job crash this time
suspend fun main() {
    val dispatcher1 = CoroutineDispatchers.io()
    val dispatcher2 = CoroutineDispatchers.data()
    val errorHandler = CoroutineExceptionHandler { context, throwable ->
        printWithThreadInfo(throwable.message + " " + context[CoroutineName])
    }
    val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("1")) {
        val job2 = launch(dispatcher2 + errorHandler + CoroutineName("2")) {
            printWithThreadInfo("11")
            delay(100)
            printWithThreadInfo("12")
            throw RuntimeException("crash")
        }
        delay(200)
        printWithThreadInfo("2")
    }
    delay(400)
    printWithThreadInfo("3")
    delay(999 * 1000L)
}
24 pool-2-thread-1 "11"
24 pool-2-thread-1 "12"
23 pool-3-thread-1 "crash CoroutineName(1)"
26 kotlinx.coroutines.DefaultExecutor "3"

conclusion

  • job2 crashed, job1 is also crashed
  • exception was handled by job1
  • child coroutine’s crash will lead to parent coroutine’s crash
  • child coroutine’s will handled by parent coroutine
Example 06
let child coroutine work longer, watch whether parent coroutine complete before child
suspend fun main() {
    val dispatcher1 = CoroutineDispatchers.io()
    val dispatcher2 = CoroutineDispatchers.data()
    val errorHandler = CoroutineExceptionHandler { context, throwable ->
        printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
    }
    lateinit var job1: Job
    job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
        val job2 = launch(dispatcher2 + errorHandler + CoroutineName("Job2")) {
            delay(100)
            printWithThreadInfo("1")
            println("Job1.isCompleted = ${job1.isCompleted}")
            throw RuntimeException("crash")
        }
        printWithThreadInfo("2")
    }
    delay(400)
    printWithThreadInfo("3")
    println("Job1.isCompleted = ${job1.isCompleted}")
    delay(999 * 1000L)
}
24 pool-3-thread-1 "2"
26 pool-2-thread-1 "1"
Job1.isCompleted = false
26 pool-2-thread-1 "Job1-crashed"
25 kotlinx.coroutines.DefaultExecutor "3"
Job1.isCompleted = true

conclusion

  • parent coroutine won’t complete, when child coroutine is still running
  • if we use GlobalScope launching job2, job1 will complete ahead, as it’s not parent this time
Example 07
start coroutine through async way
@OptIn(InternalCoroutinesApi::class)
suspend fun main() {
    val dispatcher1 = CoroutineDispatchers.io()
    val dispatcher2 = CoroutineDispatchers.data()
    val errorHandler = CoroutineExceptionHandler { context, throwable ->
        printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
    }
    val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
        val job2 = async(dispatcher2 + errorHandler + CoroutineName("Job2")) {
            delay(100)
            printWithThreadInfo("1")
            throw RuntimeException("crash")
        }
        delay(200)
        printWithThreadInfo("2")
    }
    delay(400)
    printWithThreadInfo("3")
    println("job1.isCompleted=${job1.isCompleted}")
    println("job1.isCancelled=${job1.isCancelled}")
    println("job1.exception=${job1.getCancellationException().javaClass.name}")
    delay(999 * 1000L)
}
16 pool-2-thread-1 "1"
15 pool-3-thread-1 "Job1-crashed"
17 kotlinx.coroutines.DefaultExecutor "3"
job1.isCompleted=true
job1.isCancelled=true
job1.exception=kotlinx.coroutines.JobCancellationException

conclusion

  • exception handling method for async is same with launch
  • when job2 crashed, job1 was cancelled subsequently
Example 08
test calling async in independent coroutine
suspend fun main() {
    val dispatcher1 = CoroutineDispatchers.new()
    val dispatcher2 = CoroutineDispatchers.data()
    val errorHandler = CoroutineExceptionHandler { context, throwable ->
        printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
    }
    val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
        val job2 = GlobalScope.async(dispatcher2 + errorHandler + CoroutineName("Job2")) {
            delay(200)
            printWithThreadInfo("Job2")
            throw RuntimeException("crash")
        }
        delay(400)
        printWithThreadInfo("Job1")
    }
    delay(600)
    printWithThreadInfo("Main")
    delay(999 * 1000L)
}
16 pool-2-thread-1 "Job2"
15 pool-1-thread-1 "Job1"
17 kotlinx.coroutines.DefaultExecutor "Main"

conclusion

  • everything is ok, event error handle is not called
  • as async coroutine’s exception will not be thrown until await is called
  • but when async coroutine has a parent, parent will still crashed
Example 09
test how await works after job crashed
suspend fun main() {
    val dispatcher1 = CoroutineDispatchers.new()
    val dispatcher2 = CoroutineDispatchers.data()
    val errorHandler = CoroutineExceptionHandler { context, throwable ->
        printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
    }
    val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
        val job2 = GlobalScope.async(dispatcher2 + errorHandler + CoroutineName("Job2")) {
            delay(200)
            printWithThreadInfo("Job2")
            throw RuntimeException("crash")
        }
        delay(400)
        job2.await()
        printWithThreadInfo("Job1")
    }
    delay(600)
    printWithThreadInfo("Main")
    delay(999 * 1000L)
}
16 pool-2-thread-1 "Job2"
15 pool-1-thread-1 "Job1-crashed"
17 kotlinx.coroutines.DefaultExecutor "Main"

conclusion

  • job1 crashed during await period
  • as no result can be got, await will obviously cause exception
Example 10
test how join works after job crashed
suspend fun main() {
    val dispatcher1 = CoroutineDispatchers.new()
    val dispatcher2 = CoroutineDispatchers.data()
    val errorHandler = CoroutineExceptionHandler { context, throwable ->
        printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
    }
    lateinit var job2: Job
    val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
          job2 = launch(dispatcher2 + errorHandler + CoroutineName("Job2")) {
            delay(200)
            printWithThreadInfo("Job2")
            throw RuntimeException("crash")
        }
        delay(400)
        printWithThreadInfo("Job1")
    }
    delay(600)
    job1.join()
    job2.join()
    printWithThreadInfo("Main")
    delay(1 * 1000L)
}
16 pool-2-thread-1 "Job2"
15 pool-1-thread-1 "Job1-crashed"
17 kotlinx.coroutines.DefaultExecutor "Main"

conclusion

  • job1 crashed as child coroutine crashed
  • join works well after crash
Example 11
let child coroutine handle exception by itself
suspend fun main() {
    val dispatcher1 = XDispatchers.new()
    val dispatcher2 = XDispatchers.new()
    val errorHandler = CoroutineExceptionHandler { context, throwable ->
        printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
    }
    val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
        supervisorScope {
            val job2 = launch(dispatcher2 + errorHandler + CoroutineName("Job2")) {
                delay(100)
                printWithThreadInfo("Job2")
                throw RuntimeException("crash")
            }
        }
        delay(200)
        printWithThreadInfo("Job1")
    }
    delay(400)
    printWithThreadInfo("Main")
    delay(10 * 1000L)
}
Job2 <tid=18 XDispatchers-new-2e94db>
Job2-crashed <tid=18 XDispatchers-new-2e94db>
Job1 <tid=20 XDispatchers-new-3223e7>
Main <tid=16 kotlinx.coroutines.DefaultExecutor>

conclusion

  • job2 crashed, job1 not
  • job2’s exception was handled by itself
  • if job1 cancelled, job2 will still be cancelled subsequently
  • supervisorScope just prevent exception from spreading upwords
SupervisorScope

let’s find how SupervisorScope implemented

public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = SupervisorCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}
private class SupervisorCoroutine<in T>(
    context: CoroutineContext,
    uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
    override fun childCancelled(cause: Throwable): Boolean = false
}

a SupervisorCoroutine is created, to handle block with origin context and dispatcher

SupervisorCoroutine refused handling child exception, by override childCancelled returning false

Example 12
another form using supervisor scope
suspend fun main() {
    val dispatcher1 = XDispatchers.new()
    val dispatcher2 = XDispatchers.new()
    val errorHandler = CoroutineExceptionHandler { context, throwable ->
        printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
    }
    val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
        val supervisorScope = CoroutineScope(SupervisorJob(coroutineContext[Job]))
        val job2 = supervisorScope.launch(dispatcher2 + errorHandler + CoroutineName("Job2")) {
            delay(100)
            printWithThreadInfo("Job2")
            throw RuntimeException("crash")
        }
        delay(200)
        printWithThreadInfo("Job1")
    }
    delay(400)
    printWithThreadInfo("Main")
    delay(10 * 1000L)
}
Job2 <tid=21 XDispatchers-new-24b5e7>
Job2-crashed <tid=21 XDispatchers-new-24b5e7>
Job1 <tid=22 XDispatchers-new-b88926>
Main <tid=19 kotlinx.coroutines.DefaultExecutor>

conclusion

  • this way is equivalent to supervisorScope {}
Conclusions

full process for handling coroutine exception

  • if is supervisor scope, handle by self exception handler
  • if not supervisor scope, and no parent, handle by self exception handler
  • if not supervisor scope, and has parent, handle by parent exception handler
  • if parent does not have a exception handler, use thread exception handler
  • if thread does not have a exception handler, use DefaultUncaughtExceptionHandler for Thread class
  • if DefaultUncaughtExceptionHandler not set, use global coroutine exception handler
  • if coroutine works in supervisor scope and crashed, parent will not crash, else parent will crash subsequently
  • in any case, if parent job is cancelled, child job will also be cancelled
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐