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

Как организовать обработку ошибок в корутинах? Что такое CoroutineExceptionHandler и supervisorScope?

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

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

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

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

Организация обработки ошибок в корутинах Kotlin

Обработка ошибок в корутинах — критически важный аспект для создания стабильных Android приложений. Механизмы Kotlin предоставляют несколько ключевых инструментов, основанных на принципах структурированной параллельности (structured concurrency).

Основные принципы и механизмы

В корутинах ошибки распространяются по иерархии подобно исключениям в последовательном коде, но с учётом параллельной природы:

  1. Необработанное исключение в корутине приводит к её завершению.
  2. Исключение может распространиться (propagate) на родительскую корутину или скоуп (Scope).
  3. По умолчанию, исключение в одной корутине может отменить (cancel) все другие корутины в том же скоупе (это поведение CoroutineScope).

CoroutineExceptionHandler: централизованный обработчик

CoroutineExceptionHandler — это контекстный элемент (CoroutineContext.Element), который позволяет централизованно ловить необработанные исключения в корутинах. Это особенно полезно для корутин, запущенных в скоупах верхнего уровня (например, GlobalScope или SupervisorScope), где нет автоматического распространения на родителя для обработки.

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
    // Логирование, анализ или уведомление пользователя
    Log.e("CoroutineError", "Unhandled exception: ${exception.message}", exception)
    // ВНИМАНИЕ: Не пытайтесь "восстановить" корутин здесь!
    // Этот обработчик вызывается уже после отмены корутины.
}

// Использование:
GlobalScope.launch(exceptionHandler) {
    throw RuntimeException("Test crash")
}

// ИЛИ в составе скоупа:
val customScope = CoroutineScope(Job() + exceptionHandler)

Ключевые ограничения CoroutineExceptionHandler:

  • Работает только с корутинами, запущенными через launch (не с async, поскольку там исключения "заворачиваются" в Deferred).
  • Не предотвращает отмену корутины или скоупа — это post-mortem обработчик.
  • Не должен использоваться для попытки восстановления работы — его цель логирование и анализ.

SupervisorScope и SupervisorJob: изоляция ошибок

supervisorScope и SupervisorJob меняют фундаментальное поведение распространения ошибок. В отличие от обычного CoroutineScope (где используется обычный Job), супервизор (supervisor) позволяет дочерним корутинам завершаться независимо:

// Обычный scope: ошибка в одной корутине отменяет все
coroutineScope {
    launch { delay(100); throw RuntimeException("Error 1") }
    launch { delay(200); println("This won't execute") } // Отменяется!
}

// SupervisorScope: ошибка не отменяет другие корутины
supervisorScope {
    launch { delay(100); throw RuntimeException("Error 1") }
    launch { delay(200); println("This WILL execute") } // Выполняется!
}

Практические стратегии комбинирования подходов

Для создания устойчивой системы в Android приложениях я рекомендую следующие подходы:

  1. Для UI слоя или жизненного скоупа (viewModelScope, lifecycleScope):
    • Используйте супервизорную стратегию, чтобы ошибки в одном UI-операции (например, загрузка изображения) не отменяли другие параллельные операции (обновление данных).
    • Комбинируйте с CoroutineExceptionHandler для логирования.
class MyViewModel : ViewModel() {
    private val exceptionHandler = CoroutineExceptionHandler { _, e ->
        // Отправляем ошибку в систему мониторинга
        analytics.logError(e)
    }

    fun loadMultipleData() {
        viewModelScope.launch(exceptionHandler) {
            supervisorScope {
                launch { loadUserData() } // Если ошибка здесь...
                launch { loadPosts() }     // ...эта корутина продолжит работу
                launch { loadImages() }
            }
        }
    }
}
  1. Для бизнес-логики или цепочек зависимых операций:
    • Используйте обычный coroutineScope или async/await, где ошибка должна остановить всю цепочку.
    • Обрабатывайте исключения локально через try/catch внутри корутин или через try { deferred.await() }.
suspend fun processOrder(): OrderResult = coroutineScope {
    val userInfo = async { fetchUser() }        // Если ошибка...
    val payment = async { processPayment() }    // ...все операции остановятся
    val stock = async { reserveStock() }

    // await() выбросит исключение, которое можно обработать
    try {
        OrderResult(userInfo.await(), payment.await(), stock.await())
    } catch (e: Exception) {
        // Локальная обработка и возможно retry
        throw OrderProcessingException(e)
    }
}
  1. Критически важные операции:
    • Используйте supervisorScope с индивидуальным try/catch внутри каждой корутины.
    • Комбинируйте с механизмами повторных попыток (retry) из библиотек или собственной реализации.

Итоговая рекомендация: Выбор между обычным скоупом и супервизором зависит от семантики зависимости между параллельными операциями. CoroutineExceptionHandler — это инструмент мониторинга и безопасности, но не восстановления. Локальная обработка через try/catch и правильное использование async (где исключения возвращаются как результат) остаются фундаментальными техниками. На Android всегда предпочитайте структурированные скоупы (viewModelScope, lifecycleScope) над GlobalScope для автоматического управления жизненным циклом и ошибками.

Как организовать обработку ошибок в корутинах? Что такое CoroutineExceptionHandler и supervisorScope? | PrepBro