Обработка исключений в корутинах
В этом разделе рассматриваются обработка исключений и отмена при возникновении исключений. Мы уже знаем, что отменённая корутина выбрасывает CancellationException в точках приостановки, и что механизм корутин игнорирует это исключение. Здесь мы рассмотрим, что происходит, если исключение выбрасывается во время отмены или если несколько дочерних корутин одной и той же корутины выбрасывают исключения.
Распространение исключений
Создатели корутин бывают двух видов: автоматически распространяющие исключения (launch) и предоставляющие их пользователям (async и produce). Когда эти создатели используются для создания корневой корутины, то есть не являющейся дочерней для другой корутины, первые создатели обрабатывают исключения как неперехваченные, подобно Thread.uncaughtExceptionHandler в Java, тогда как вторые полагаются на то, что пользователь обработает итоговое исключение, например, с помощью await или receive (создатель produce и receive рассматриваются в разделе Каналы).
Это можно продемонстрировать на простом примере, создающем корневые корутины с помощью GlobalScope:
import kotlinx.coroutines.*
//sampleStart
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
val job = GlobalScope.launch { // root coroutine with launch
println("Throwing exception from launch")
throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
}
job.join()
println("Joined failed job")
val deferred = GlobalScope.async { // root coroutine with async
println("Throwing exception from async")
throw ArithmeticException() // Nothing is printed, relying on user to call await
}
try {
deferred.await()
println("Unreached")
} catch (e: ArithmeticException) {
println("Caught ArithmeticException")
}
}
//sampleEnd
Результат выполнения этого кода (с включённой отладкой):
Throwing exception from launch Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.IndexOutOfBoundsException Joined failed job Throwing exception from async Caught ArithmeticException
CoroutineExceptionHandler
Стандартное поведение, при котором неперехваченные исключения выводятся в консоль, можно настроить. Элемент контекста CoroutineExceptionHandler в корневой корутине можно использовать как универсальный блок catch для этой корневой корутины и всех её дочерних корутин, чтобы выполнять пользовательскую обработку исключений. Он аналогичен Thread.uncaughtExceptionHandler. В CoroutineExceptionHandler нельзя восстановиться после исключения. К моменту вызова обработчика корутина уже завершилась с соответствующим исключением. Обычно обработчик используется для записи исключения в журнал, вывода сообщения об ошибке, завершения и/или перезапуска приложения.
CoroutineExceptionHandler вызывается только для неперехваченных исключений — тех, которые не были обработаны иным способом. В частности, все корутины-потомки (корутины, созданные в контексте другой Job) делегируют обработку своих исключений родительской корутине, которая, в свою очередь, делегирует её своей родительской корутине, и так далее вплоть до корневой. Поэтому установленный в их контексте CoroutineExceptionHandler никогда не используется. Кроме того, создатель async всегда перехватывает все исключения и представляет их в результирующем объекте Deferred, поэтому его CoroutineExceptionHandler также не оказывает эффекта.
import kotlinx.coroutines.*
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)
//sampleEnd
}
Результат выполнения этого кода:
CoroutineExceptionHandler got java.lang.AssertionError
Отмена и исключения
Отмена тесно связана с исключениями. Для отмены корутины внутри используют CancellationException; все обработчики игнорируют эти исключения, поэтому их следует использовать только как источник дополнительной отладочной информации, которую можно получить с помощью блока catch. Когда корутина отменяется с помощью Job.cancel, она завершается, но не отменяет родительскую корутину.
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val job = launch {
val child = launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("Child is cancelled")
}
}
yield()
println("Cancelling child")
child.cancel()
child.join()
yield()
println("Parent is not cancelled")
}
job.join()
//sampleEnd
}
Результат выполнения этого кода:
Cancelling child Child is cancelled Parent is not cancelled
Если в корутине возникает исключение, отличное от CancellationException, она отменяет родительскую корутину с этим исключением. Это поведение нельзя переопределить; оно обеспечивает стабильную иерархию корутин для структурированной конкурентности. Реализация CoroutineExceptionHandler не используется для дочерних корутин.
Родитель обрабатывает исходное исключение только после завершения всех дочерних корутин, что демонстрирует следующий пример.
import kotlinx.coroutines.*
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
launch { // the first child
try {
delay(Long.MAX_VALUE)
} finally {
withContext(NonCancellable) {
println("Children are cancelled, but exception is not handled until all children terminate")
delay(100)
println("The first child finished its non cancellable block")
}
}
}
launch { // the second child
delay(10)
println("Second child throws an exception")
throw ArithmeticException()
}
}
job.join()
//sampleEnd
}
Результат выполнения этого кода:
Second child throws an exception Children are cancelled, but exception is not handled until all children terminate The first child finished its non cancellable block CoroutineExceptionHandler got java.lang.ArithmeticException
Объединение исключений
Если несколько дочерних корутин завершаются с исключением, действует общее правило: «побеждает первое исключение», поэтому обрабатывается первое исключение. Все дополнительные исключения, возникшие после первого, добавляются к нему как подавленные.
import kotlinx.coroutines.*
import java.io.*
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
try {
delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
} finally {
throw ArithmeticException() // the second exception
}
}
launch {
delay(100)
throw IOException() // the first exception
}
delay(Long.MAX_VALUE)
}
job.join()
}
Результат выполнения этого кода:
CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]
Исключения отмены прозрачны и по умолчанию разворачиваются:
import kotlinx.coroutines.*
import java.io.*
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
val innerJob = launch { // all this stack of coroutines will get cancelled
launch {
launch {
throw IOException() // the original exception
}
}
}
try {
innerJob.join()
} catch (e: CancellationException) {
println("Rethrowing CancellationException with original cause")
throw e // cancellation exception is rethrown, yet the original IOException gets to the handler
}
}
job.join()
//sampleEnd
}
Результат выполнения этого кода:
Rethrowing CancellationException with original cause CoroutineExceptionHandler got java.io.IOException
Надзор
Как мы уже изучили ранее, сбой корутины распространяется в обоих направлениях по всей иерархии корутин. Рассмотрим случай, когда требуется однонаправленное распространение сбоев.
Хороший пример такой ситуации — компонент пользовательского интерфейса с задачей, определённой в его области видимости. Если одна из дочерних задач пользовательского интерфейса завершается с ошибкой, не всегда необходимо отменять (фактически уничтожать) весь компонент. Однако если компонент пользовательского интерфейса уничтожается (и его задача отменяется), необходимо отменить все дочерние задачи, поскольку их результаты больше не нужны.
Другой пример — серверный процесс, запускающий несколько дочерних задач и нуждающийся в том, чтобы контролировать их выполнение, отслеживать сбои и перезапускать только задачи, завершившиеся с ошибкой.
Задача надзора
Для этих целей можно использовать SupervisorJob. Она похожа на обычную Job, за исключением того, что сбой или отмена дочерней задачи не распространяется на задачу надзора или её другие дочерние задачи. Это легко продемонстрировать на следующем примере:
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
// launch the first child -- its exception is ignored for this example (don't do this in practice!)
val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
println("The first child is failing")
throw AssertionError("The first child is cancelled")
}
// launch the second child
val secondChild = launch {
firstChild.join()
// Cancellation of the first child is not propagated to the second child
println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
try {
delay(Long.MAX_VALUE)
} finally {
// But cancellation of the supervisor is propagated
println("The second child is cancelled because the supervisor was cancelled")
}
}
// wait until the first child fails & completes
firstChild.join()
println("Cancelling the supervisor")
supervisor.cancel()
secondChild.join()
}
//sampleEnd
}
Результат выполнения этого кода:
The first child is failing The first child is cancelled: true, but the second one is still active Cancelling the supervisor The second child is cancelled because the supervisor was cancelled
Область надзора
Вместо coroutineScope для конкурентности в пределах области видимости можно использовать supervisorScope. Она распространяет отмену только в одном направлении и отменяет все дочерние корутины, только если сама завершается с ошибкой. Кроме того, подобно coroutineScope, она ожидает завершения всех дочерних корутин.
import kotlin.coroutines.*
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
try {
supervisorScope {
val child = launch {
try {
println("The child is sleeping")
delay(Long.MAX_VALUE)
} finally {
println("The child is cancelled")
}
}
// Give our child a chance to execute and print using yield
yield()
println("Throwing an exception from the scope")
throw AssertionError()
}
} catch(e: AssertionError) {
println("Caught an assertion error")
}
//sampleEnd
}
Результат выполнения этого кода:
The child is sleeping Throwing an exception from the scope The child is cancelled Caught an assertion error
Исключения в корутинах под надзором
Ещё одно важное различие между обычными задачами и задачами надзора заключается в обработке исключений. Каждая дочерняя корутина должна самостоятельно обрабатывать свои исключения с помощью механизма обработки исключений. Это различие обусловлено тем, что сбой дочерней корутины не распространяется на родительскую. Это означает, что корутины, запущенные непосредственно внутри supervisorScope, действительно используют установленный в их области видимости CoroutineExceptionHandler так же, как это делают корневые корутины (подробности см. в разделе CoroutineExceptionHandler).
import kotlin.coroutines.*
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
supervisorScope {
val child = launch(handler) {
println("The child throws an exception")
throw AssertionError()
}
println("The scope is completing")
}
println("The scope is completed")
//sampleEnd
}
Результат выполнения этого кода:
The scope is completing The child throws an exception CoroutineExceptionHandler got java.lang.AssertionError The scope is completed
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/exception-handling.html