KotlinSeniorTechnical
Как обрабатывать исключения в Kotlin coroutines?
launch пробрасывает исключение в родителя (отменяет дерево), async хранит его в Deferred до await(). CancellationException нельзя проглатывать — её нужно перебрасывать. SupervisorJob изолирует сбои child coroutines.
Модель распространения исключений
Поведение исключений в coroutines зависит от двух вещей: какой builder использован (launch или async) и является ли coroutine root (запущена напрямую в scope) или child (запущена внутри другой coroutine).
launch— uncaught exception немедленно пробрасывается в родительский Job и отменяет всё дерево coroutines.async— исключение хранится вDeferredи выбрасывается при вызове.await().CancellationException— особый случай: она не является «настоящей» ошибкой и не отменяет родителя; проглатывать её нельзя.
CoroutineExceptionHandler
import kotlinx.coroutines.*
val handler = CoroutineExceptionHandler { _, throwable ->
println("Caught: ${throwable.message}")
}
fun main() = runBlocking {
// handler работает только для root coroutines (launch в scope верхнего уровня)
val job = launch(handler) {
throw RuntimeException("Something went wrong")
}
job.join()
println("Продолжаем работу")
}
// Вывод:
// Caught: Something went wrong
// Продолжаем работу
SupervisorJob и supervisorScope
С обычным Job сбой одного child отменяет всех остальных. SupervisorJob изолирует сбои: остальные coroutines продолжают работать.
fun main() = runBlocking {
// supervisorScope — каждый child независим
supervisorScope {
val a = launch {
delay(100)
throw RuntimeException("Child A failed")
}
val b = launch {
delay(200)
println("Child B completed") // выполнится, несмотря на сбой A
}
// Исключение из a нужно поймать явно
try {
a.join()
} catch (e: Exception) {
println("Handled: ${e.message}")
}
b.join()
}
}
async + await: правильный паттерн
fun main() = runBlocking {
val deferred = async {
delay(50)
throw IOException("Network error")
"result"
}
try {
val result = deferred.await() // исключение выбросится здесь
println(result)
} catch (e: IOException) {
println("IO error: ${e.message}")
}
}
Правило CancellationException
suspend fun doWork() {
try {
delay(1000)
} catch (e: CancellationException) {
println("Coroutine cancelled — cleanup")
throw e // ОБЯЗАТЕЛЬНО перебросить! Иначе отмена не распространится
} catch (e: Exception) {
println("Real error: ${e.message}")
}
}
Подводные камни
- Проглатывание CancellationException — ловить
ThrowableилиExceptionи не перебрасыватьCancellationExceptionнарушает cooperative cancellation: coroutine продолжает выполняться после отмены, что ведёт к утечкам. - CoroutineExceptionHandler не работает для async — исключение из
asyncне попадает в handler; его нужно ловить приawait(). - CoroutineExceptionHandler не работает для child coroutines — handler работает только для root coroutine (запущенной непосредственно в
CoroutineScope), не для вложенных. - runBlocking перебрасывает исключение — в отличие от launch,
runBlockingвыбрасывает исключение в вызывающий поток; не оборачивайте его в производственном коде без обработки. - Job vs SupervisorJob в ViewModel — Android ViewModel имеет
viewModelScopeсSupervisorJob; если вручную создать scope с обычнымJob, один сбой убьёт все корутины ViewModel. - Множественные исключения (AggregateException) — если несколько child coroutines в обычном scope падают одновременно, только первое исключение станет основным; остальные добавятся как suppressed через
e.suppressed. - withContext не изолирует ошибки — исключение внутри
withContextпробрасывается в родительскую coroutine как обычное исключение;withContextне является supervisor.
Common mistakes
- Объяснять «исключения в coroutines» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Самая опасная ошибка - ловить Throwable/Exception и проглатывать CancellationException, ломая cooperative cancellation.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «исключения в coroutines» своими словами и связывает ее с кодом.
- Называет механизм: launch пробрасывает uncaught exception в родителя, async хранит его в Deferred до await, cancellation использует CancellationException и должна сохраняться.
- Видит production-последствие: Самая опасная ошибка - ловить Throwable/Exception и проглатывать CancellationException, ломая cooperative cancellation.