Как организовать обработку ошибок в корутинах? Что такое CoroutineExceptionHandler и supervisorScope?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Организация обработки ошибок в корутинах Kotlin
Обработка ошибок в корутинах — критически важный аспект для создания стабильных Android приложений. Механизмы Kotlin предоставляют несколько ключевых инструментов, основанных на принципах структурированной параллельности (structured concurrency).
Основные принципы и механизмы
В корутинах ошибки распространяются по иерархии подобно исключениям в последовательном коде, но с учётом параллельной природы:
- Необработанное исключение в корутине приводит к её завершению.
- Исключение может распространиться (propagate) на родительскую корутину или скоуп (Scope).
- По умолчанию, исключение в одной корутине может отменить (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 приложениях я рекомендую следующие подходы:
- Для 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() }
}
}
}
}
- Для бизнес-логики или цепочек зависимых операций:
- Используйте обычный
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)
}
}
- Критически важные операции:
- Используйте
supervisorScopeс индивидуальнымtry/catchвнутри каждой корутины. - Комбинируйте с механизмами повторных попыток (
retry) из библиотек или собственной реализации.
- Используйте
Итоговая рекомендация: Выбор между обычным скоупом и супервизором зависит от семантики зависимости между параллельными операциями. CoroutineExceptionHandler — это инструмент мониторинга и безопасности, но не восстановления. Локальная обработка через try/catch и правильное использование async (где исключения возвращаются как результат) остаются фундаментальными техниками. На Android всегда предпочитайте структурированные скоупы (viewModelScope, lifecycleScope) над GlobalScope для автоматического управления жизненным циклом и ошибками.