← Назад к вопросам

Пробрасывается ли ошибка отмены корутины вверх всегда в корутинах

2.0 Middle🔥 181 комментариев
#Kotlin основы#Многопоточность и асинхронность

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отличный вопрос, затрагивающий один из фундаментальных и тонких аспектов Kotlin Coroutines. Прямой ответ — нет, ошибка отмены (CancellationException) вверх НЕ пробрасывается всегда. Это осознанное проектное решение, которое отличает отмену от обычных ошибок.

Давайте разберем этот механизм детально.

Ключевое понятие: Структурная отмена и CancellationException

Структурная параллелизм в корутинах означает, что новая корутина запускается в определенной области видимости (CoroutineScope), и эта область контролирует ее жизненный цикл. Когда родительская область отменяется, она последовательно отменяет все свои дочерние корутины.

Отмена корутины реализована через бросание специального исключения — CancellationException (или его подкласса JobCancellationException). Однако это исключение обрабатывается особым образом внутри механизма корутин.

Почему CancellationException не прокидывается "вверх"?

  1. Отмена — это нормальный жизненный цикл, а не ошибка. Цель отмены — кооперативно (сотрудничая) остановить выполнение. Если бы CancellationException всегда прокидывался до верхнего уровня, он ломал бы стандартные конструкции обработки ошибок (try-catch), и разработчикам пришлось бы постоянно его фильтровать.
  2. Предотвращение "загрязнения" кода. Пользовательские обработчики в CoroutineExceptionHandler или try-catch в корне обычно предназначены для обработки бизнес-ошибок (сетевая ошибка, ошибка парсинга), а не служебного механизма отмены.
  3. Изоляция областей видимости. Дочерняя корутина может быть отменена независимо от родительской. Если бы ее отмена всегда прокидывалась родителю, это нарушало бы принцип изоляции и делало управление отменой слишком хрупким.

Поведение на практике

Рассмотрим ключевые моменты на примерах.

Пример 1: Отмена не ломает try-catch для других исключений

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            delay(1000) // Имитация работы
            println("Работа завершена")
        } catch (e: Exception) {
            // CancellationException сюда НЕ попадет, если отмена произошла во время delay/suspend функции
            println("Поймано исключение: ${e.javaClass.simpleName}")
        }
    }
    delay(100) // Ждем немного
    job.cancelAndJoin() // Отменяем корутину
    println("Основной поток завершен")
}

Вывод:

Основной поток завершен

CancellationException, брошенный delay() при отмене, НЕ был пойман блоком catch. Он был поглощен внутренним механизмом корутины для завершения работы.

Пример 2: CancellationException пробрасывается, если его бросить вручную

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("Job: Я считаю $i ...")
                if (i == 5) {
                    // Вручную бросаем CancellationException внутри выполняющегося кода (не в suspend функции)
                    throw CancellationException("Я устал считать!")
                }
                yield() // Кооперативная функция, проверяющая отмену
            }
        } catch (e: CancellationException) {
            // Теперь мы его поймаем!
            println("Поймана отмена: ${e.message}")
            throw e // ОБЯЗАТЕЛЬНО пробросить дальше, иначе корутина не завершится корректно
        }
    }
    job.join()
    println("Основной поток завершен")
}

Здесь исключение брошено вручную в теле корутины и будет обработано стандартным блоком catch.

Пример 3: Поведение с другими исключениями

Если в корутине возникает любое другое исключение (не CancellationException), оно, по умолчанию, пробрасывается вверх и может:

  1. Отменить родительскую корутину (по умолчанию).
  2. Быть обработано CoroutineExceptionHandler в корневых скоупах (например, у SupervisorJob или GlobalScope).
import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler получил: ${exception.javaClass.simpleName}")
    }
    val job = launch(handler) { // Обратите внимание: handler здесь не сработает, так как launch наследует контекст runBlocking
        launch(handler) { // А здесь сработает, если используется SupervisorJob
            throw ArithmeticException("Ой, деление на ноль!")
        }
    }
    job.join()
}

Правила и лучшие практики

  1. Корневые обработчики: CoroutineExceptionHandler установленный в корне скоупа (например, у scope.launch(CoroutineExceptionHandler { ... })) не получает CancellationException. Он предназначен только для необработанных, не-отменяющих исключений.
  2. Suspend функции: Почти все стандартные suspend функции (delay(), withContext(), join() и др.) проверяют отмену корутины и, будучи отмененными, бросают CancellationException, который поглощается внутренне.
  3. Ручная проверка: Внутри долгих вычислений (не-suspend блоков) необходимо вручную проверять статус отмены через ensureActive() или isActive.
    launch {
        for (i in 1..1_000_000) {
            ensureActive() // Выбросит CancellationException, если корутина отменена
            // ... тяжелые вычисления ...
        }
    }
    
  4. finally блоки: Код в блоке finally выполняется даже при отмене корутины, что идеально подходит для освобождения ресурсов.
    launch {
        val resource = acquireResource()
        try {
            // работа с ресурсом
            delay(500)
        } finally {
            // Этот блок выполнится гарантированно!
            resource.release()
            println("Ресурс освобожден даже при отмене")
        }
    }
    

Итог

CancellationException является особым, "тихим" исключением. Он пробрасывается вверх по стеку вызовов корутины только в том случае, если был брошен явно в коде (не suspend-функцией) и не был пойман. Основная его задача — прервать выполнение suspend-функций и завершить корутину, не вмешиваясь в пользовательскую логику обработки настоящих ошибок. Это гениальное решение, которое делает работу с отменой в Kotlin Coroutines предсказуемой и чистой. Понимание этого различия критически важно для написания стабильного и корректно останавливаемого асинхронного кода.

Пробрасывается ли ошибка отмены корутины вверх всегда в корутинах | PrepBro