Spec-Zone.ru › Kotlin 2

Обработка исключений в корутинах

В этом разделе рассматриваются обработка исключений и отмена при возникновении исключений. Мы уже знаем, что отменённая корутина выбрасывает CancellationException в точках приостановки, и что механизм корутин игнорирует это исключение. Здесь мы рассмотрим, что происходит, если исключение выбрасывается во время отмены или если несколько дочерних корутин одной и той же корутины выбрасывают исключения.

Распространение исключений

Создатели корутин бывают двух видов: автоматически распространяющие исключения (launch) и предоставляющие их пользователям (async и produce). Когда эти создатели используются для создания корневой корутины, то есть не являющейся дочерней для другой корутины, первые создатели обрабатывают исключения как неперехваченные, подобно Thread.uncaughtExceptionHandler в Java, тогда как вторые полагаются на то, что пользователь обработает итоговое исключение, например, с помощью await или receive (создатель produce и receive рассматриваются в разделе Каналы).

Это можно продемонстрировать на простом примере, создающем корневые корутины с помощью GlobalScope:

GlobalScope — это деликатный API, использование которого может привести к неожиданным последствиям. Создание корневой корутины для всего приложения — один из редких обоснованных случаев использования GlobalScope, поэтому для использования GlobalScope необходимо явно указать согласие с помощью @OptIn(DelicateCoroutinesApi::class).

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 не используется для дочерних корутин.

В этих примерах CoroutineExceptionHandler всегда устанавливается для корутины, созданной в GlobalScope. Устанавливать обработчик исключений для корутины, запущенной в области видимости основного runBlocking, бессмысленно: основная корутина всё равно будет отменена, когда её дочерняя корутина завершится с исключением, несмотря на установленный обработчик.

Родитель обрабатывает исходное исключение только после завершения всех дочерних корутин, что демонстрирует следующий пример.

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]

Обратите внимание, что в настоящее время этот механизм работает только в Java версии 1.7 и выше. Ограничения JS и Native носят временный характер и будут сняты в будущем.

Исключения отмены прозрачны и по умолчанию разворачиваются:

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
20 июля 2026
КаналыОбщее изменяемое состояние и конкурентность

© 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API