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

В чем разница между обработкой обычных исключений и исключений корутин?

3.0 Senior🔥 142 комментариев
#Kotlin основы#Многопоточность и асинхронность

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

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

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

Разница между обработкой обычных исключений и исключений корутин

Обработка исключений в традиционных (обычных) синхронных программах и в корутинных (асинхронных) контекстах на Kotlin имеет существенные различия, обусловленные особенностями асинхронного выполнения и структурой корутин. Эти различия затрагивают механизмы распространения, перехвата и управления исключениями.

1. Механизм распространения исключений

В обычных синхронных функциях исключение немедленно выбрасывается в точке возникновения и распространяется по стеку вызовов до первого подходящего блока catch или, если его нет, приводит к краху потока/приложения.

fun traditionalExceptionExample() {
    try {
        riskyFunction()
    } catch (e: Exception) {
        println("Перехвачено: ${e.message}")
    }
}

fun riskyFunction() {
    throw IllegalStateException("Ошибка в синхронной функции!")
}

В корутинах исключение не выбрасывается немедленно в вызывающий код. Вместо этого оно "заворачивается" в состояние завершения корутины (например, CancellationException или другие) и может быть обработано внутри контекста этой корутины или распространено на родительскую корутину в иерархии Job/Scope.

suspend fun coroutineExceptionExample() {
    coroutineScope {
        try {
            launch {
                riskyCoroutineFunction()
            }
        } catch (e: Exception) {
            // Этот catch НЕ перехватит исключение из launch!
            println("Это не сработает: ${e.message}")
        }
    }
}

suspend fun riskyCoroutineFunction() {
    throw IllegalStateException("Ошибка в корутине!")
}

2. Иерархия Job и родительская ответственность

Корутины организованы в иерархии через Job и CoroutineScope. Это ключевое отличие влияет на обработку исключений:

  • Родительская Job может реагировать на исключения из своих детей.
  • По умолчанию, если исключение не перехвачено внутри корутины (например, с помощью try-catch внутри launch блока), оно приводит к отмену родительской Job и всех других детей в этом Scope.
fun parentChildExceptionHandling() = runBlocking {
    val handler = CoroutineExceptionHandler { context, exception ->
        println("Перехвачено в ExceptionHandler: ${exception.message}")
    }
    
    val scope = CoroutineScope(Job() + handler)
    scope.launch {
        launch {
            delay(100)
            throw ArithmeticException("Дочерняя корутина упала!")
        }
        launch {
            delay(200)
            println("Эта корутина будет отменена из-за исключения в sibling")
        }
    }
    // Родительская Job и все дети будут отменены из-за необработанного исключения
}

3. Различия между Builder'ами: launch и async

  • launch: Используется для "fire-and-forget" операций. Необработанные исключения внутри launch сразу приводят к отмене родительского Scope (если нет установленного CoroutineExceptionHandler).
  • async: Возвращает Deferred<T>. Исключение внутри async блока не выбрасывается сразу, а оборачивается в состояние Deferred. Оно будет выброшено только при вызове .await() на этом Deferred.
fun asyncExceptionExample() = runBlocking {
    val deferred = async {
        throw IllegalArgumentException("Ошибка внутри async!")
        "Результат"
    }
    
    try {
        deferred.await() // Исключение выбрасывается здесь, при попытке получить результат
    } catch (e: Exception) {
        println("Перехвачено при await: ${e.message}")
    }
}

4. Использование CoroutineExceptionHandler

Для централизованной обработки необработанных исключений из корутин используется CoroutineExceptionHandler. Это специальный элемент контекста корутин, который работает ТОЛЬКО с корутинами верхнего уровня (например, созданными через launch без родителя или в SupervisorJob).

fun exceptionHandlerExample() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Глобальный обработчик перехватил: ${exception.message}")
    }
    
    val scope = CoroutineScope(Job() + handler)
    scope.launch { // Корутина верхнего уровня
        throw RuntimeException("Тестовое исключение")
    }
    delay(500) // Даем время для выполнения
}

5. SupervisorJob и изоляция исключений

SupervisorJob — специальный тип родительской Job, который изменяет поведение распространения исключений. Исключение в одной дочерней корутине не приводит к автоматической отмены других детей или родителя.

fun supervisorJobExample() = runBlocking {
    val supervisorScope = CoroutineScope(SupervisorJob())
    
    supervisorScope.launch {
        delay(100)
        throw Exception("Первая корутина падает!")
    }
    
    supervisorScope.launch {
        delay(200)
        println("Эта корутина НЕ отменена и выполнится!")
    }
    
    delay(300)
}

Ключевые выводы и лучшие практики

  • Традиционные исключения линейны и немедленны. Корутинные исключения асинхронны и связаны с иерархией Job.
  • Для перехвата исключений внутри корутины используйте try-catch непосредственно в теле корутины (внутри launch или async блока).
  • Для глобальной обработки необработанных исключений используйте CoroutineExceptionHandler на корутинах верхнего уровня.
  • async запаковывает исключения в Deferred, делая их обработку явной при await().
  • Используйте SupervisorJob или supervisorScope, когда нужно изолировать отмены корутин друг от друга (например, в UI или независимых задачах).
  • Помните, что CancellationException — особый тип исключения в корутинах, который обычно не приводит к отмене родителя и часто обрабатывается внутренне для корректного завершения ресурсов.

Понимание этих различий критически важно для создания устойчивых асинхронных приложений на Kotlin, где управление ошибками напрямую влияет на стабильность и пользовательский опыт.

В чем разница между обработкой обычных исключений и исключений корутин? | PrepBro