---
title: Исключения в корутинах
description: Как ошибки идут по дереву, разница launch и async, SupervisorJob, CoroutineExceptionHandler и CancellationException
tags:
- kotlin
- coroutines
---
Обработка ошибок в корутинах отличается от обычной именно потому, что корутина - не вызов функции: исключение возникает не там, где его ждёт `try`, а внутри другой корутины.
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val result = runCatching {
coroutineScope {
launch { throw IllegalStateException("ошибка внутри") }
}
}
println(result.exceptionOrNull()?.message) // ошибка внутри
}
```
Ошибка из `launch` не выбрасывается в месте вызова `launch` - она идёт **вверх по дереву** и проявляется на границе области.
## Два механизма: launch и async
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
// launch: ошибка идёт родителю сразу
val fromLaunch = runCatching {
coroutineScope { launch { error("из launch") } }
}.exceptionOrNull()?.message
// async: ошибка хранится в Deferred и выбрасывается при await
val fromAsync = runCatching {
coroutineScope {
val d = async { error("из async") }
d.await()
}
}.exceptionOrNull()?.message
println("$fromLaunch | $fromAsync") // из launch | из async
}
```
Разница в том, **когда** вы увидите ошибку:
| | `launch` | `async` |
|---|---|---|
| куда идёт ошибка | сразу родителю | сохраняется в `Deferred` |
| где ловить | `try` вокруг области, обработчик | `try` вокруг `await()` |
| если не обработать | падает область | при `coroutineScope` - тоже падает область |
| если `async` висит в supervisor | - | ошибка потеряется совсем |
Ключевой момент: `async` в обычной области **всё равно** передаёт ошибку родителю, даже без `await()`. Исключение в этом случае попадает в родителя при завершении области.
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val result = runCatching {
coroutineScope {
async { error("await не вызывали") }
delay(20)
"тело дошло до конца"
}
}
println(result.exceptionOrNull()?.message) // await не вызывали
}
```
А вот в supervisor-области непрочитанная ошибка `async` исчезает:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
supervisorScope {
async { error("никто не узнает") } // await не вызван
delay(20)
println("ни падения, ни лога")
}
// ни падения, ни лога
}
```
Это самый тихий способ потерять ошибку в корутинах.
## Как ошибка идёт по дереву
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val result = runCatching {
coroutineScope {
launch { // уровень 1
launch { // уровень 2
launch { // уровень 3
throw IllegalStateException("с третьего уровня")
}
}
}
launch {
try { delay(1000) } finally { println("соседнюю ветку отменили") }
}
}
}
println("дошло до корня: ${result.exceptionOrNull()?.message}")
// соседнюю ветку отменили
// дошло до корня: с третьего уровня
}
```
Механика: корутина, получившая ошибку, отменяет себя, передаёт исключение родителю, тот отменяет **всех остальных детей** и повторяет процедуру выше. Промежуточные уровни не могут «не пропустить» ошибку - её нужно ловить `try/catch` внутри самой корутины.
```mermaid
graph BT
D["Ребёнок уровня 3
throw"] -->|"ошибка наверх"| C["Ребёнок уровня 2"]
C -->|"ошибка наверх"| B["Ребёнок уровня 1"]
B -->|"ошибка наверх"| A["Корень области"]
A -->|"отменить"| E["Соседняя ветка"]
A --> F{"есть обработчик?"}
F -->|"да"| G["CoroutineExceptionHandler"]
F -->|"нет"| H["Thread.uncaughtExceptionHandler
падение"]
```
## Где try/catch работает
Внутри самой корутины - как обычно:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
try {
error("локальная ошибка")
} catch (e: IllegalStateException) {
println("обработали на месте: ${e.message}")
}
}.join()
println("родитель не затронут")
// обработали на месте: локальная ошибка
// родитель не затронут
}
```
А вокруг `launch` - нет:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch(SupervisorJob()) { // отдельный Job, чтобы не уронить main
try {
launch { error("из вложенного launch") }
} catch (e: Exception) {
println("этого не будет")
}
}
job.join()
println("родитель выжил: ${job.isCancelled}")
// родитель выжил: true
}
```
`launch` возвращается мгновенно, до того как тело начало выполняться - ловить там нечего.
## SupervisorJob
Супервизор меняет направление: ошибка ребёнка не идёт вверх.
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, e ->
println("обработчик: ${e.message}")
}
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch { error("первая задача упала") }
delay(20)
scope.launch { println("вторая работает как ни в чём не бывало") }
delay(20)
println("scope жив: ${scope.isActive}")
scope.cancel()
// обработчик: первая задача упала
// вторая работает как ни в чём не бывало
// scope жив: true
}
```
Сравните с обычным `Job`:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, e -> println("обработчик: ${e.message}") }
val scope = CoroutineScope(Job() + handler)
scope.launch { error("первая задача упала") }
delay(20)
val second = scope.launch { println("не запустится") }
delay(20)
println("scope мёртв: ${!scope.isActive}, вторая отменена: ${second.isCancelled}")
// обработчик: первая задача упала
// scope мёртв: true, вторая отменена: true
}
```
Правило: долгоживущий scope компонента - `SupervisorJob`; группа задач внутри одной операции - обычный `Job` через `coroutineScope`.
## supervisorScope против coroutineScope
```kotlin
import kotlinx.coroutines.*
suspend fun loadAllOrFail(): String = coroutineScope {
val a = async { delay(20); "A" }
val b = async { error("B упал"); "B" }
"${a.await()}${b.await()}"
}
suspend fun loadWhatCan(): List = supervisorScope {
val a = async { delay(20); "A" }
val b = async { error("B упал") }
listOf(
runCatching { a.await() }.getOrElse { "?" },
runCatching { b.await() }.getOrElse { "?" },
)
}
fun main() = runBlocking {
println(runCatching { loadAllOrFail() }.exceptionOrNull()?.message) // B упал
println(loadWhatCan()) // [A, ?]
}
```
Первый вариант - «нужны все данные», второй - «покажем что есть». В supervisor-области каждый `await()` нужно обрабатывать отдельно, иначе ошибка улетит в обработчик по умолчанию.
## CoroutineExceptionHandler
Это «последняя линия» - обработчик необработанных ошибок, аналог `Thread.UncaughtExceptionHandler`.
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { context, e ->
println("необработанная ошибка в ${context[CoroutineName]?.name}: ${e.message}")
}
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch(CoroutineName("загрузчик")) { error("сеть недоступна") }
delay(20)
scope.cancel()
// необработанная ошибка в загрузчик: сеть недоступна
}
```
Три правила, которые не очевидны:
**1. Обработчик работает только в корневой корутине.** В дочерней он игнорируется:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, e -> println("вызван: ${e.message}") }
val scope = CoroutineScope(SupervisorJob())
scope.launch {
launch(handler) { error("ошибка в ребёнке") } // handler проигнорирован
}
delay(50)
scope.cancel()
// Exception in thread ... IllegalStateException: ошибка в ребёнке
}
```
Ошибка ушла наверх, а наверху обработчика нет. Ставить `handler` нужно в контекст scope или корневого билдера.
**2. `async` обработчик не использует** - его ошибка принадлежит `Deferred`:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, e -> println("не будет вызван") }
val scope = CoroutineScope(SupervisorJob() + handler)
val d = scope.async { error("ошибка в async") }
delay(20)
println("ошибка ждёт в Deferred: ${runCatching { d.await() }.isFailure}")
scope.cancel()
// ошибка ждёт в Deferred: true
}
```
**3. `runBlocking` обработчик не использует** - он выбрасывает исключение наружу, в вызывающий поток.
## CancellationException - не ошибка
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
launch {
delay(1000)
}.cancel() // отмена ребёнка
delay(20)
println("родитель продолжает работу")
}
job.join()
println("родитель завершился нормально: ${!job.isCancelled}")
// родитель продолжает работу
// родитель завершился нормально: true
}
```
`CancellationException`, выброшенное корутиной, трактуется как «я завершилась по запросу» и наверх не передаётся. Именно поэтому `withTimeout` внутри одной корутины не роняет соседей. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md)
Обратная сторона: `CancellationException` нельзя использовать как обычное исключение - оно будет молча проигнорировано:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch(SupervisorJob()) {
throw CancellationException("это не ошибка, а отмена")
}
job.join()
println("отменён, а не упал: ${job.isCancelled}") // отменён, а не упал: true
}
```
## Агрегация ошибок
Родитель получает **первое** исключение. Ошибки, возникшие уже во время отмены - например, в блоке `finally` соседней корутины - не теряются, а попадают в `suppressed`:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val error = runCatching {
coroutineScope {
launch { delay(10); throw IllegalStateException("первая") }
launch {
try { delay(1000) } finally { throw IllegalArgumentException("из finally") }
}
}
}.exceptionOrNull()
println("основная: ${error?.message}")
println("подавленных: ${error?.suppressed?.size}")
println("подавленная: ${error?.suppressed?.firstOrNull()?.message}")
// основная: первая
// подавленных: 1
// подавленная: из finally
}
```
Важно, чего здесь **нет**: если бы вторая корутина просто бросила исключение через 30 мс, подавленной ошибки не появилось бы - её отменили бы на 10-й миллисекунде, и она никогда не дошла бы до `throw`. Fail-fast означает именно это: вторая ошибка обычно не возникает, потому что работу остановили. В `suppressed` попадают только сбои очистки.
Практический вывод для логирования: печатать нужно полный стектрейс (`printStackTrace()` или `logger.error(msg, e)`), а не `e.message` - иначе сбои в `finally` останутся невидимыми.
## Повторные попытки
```kotlin
import kotlinx.coroutines.*
suspend fun retry(times: Int, delayMs: Long, block: suspend () -> T): T {
var last: Throwable? = null
repeat(times) { attempt ->
try {
return block()
} catch (e: CancellationException) {
throw e // отмену не ретраим!
} catch (e: Exception) {
last = e
println("попытка ${attempt + 1} не удалась: ${e.message}")
delay(delayMs)
}
}
throw last ?: IllegalStateException("нет попыток")
}
fun main() = runBlocking {
var attempts = 0
val result = retry(3, 10) {
attempts++
if (attempts < 3) error("сеть недоступна") else "данные получены"
}
println(result)
// попытка 1 не удалась: сеть недоступна
// попытка 2 не удалась: сеть недоступна
// данные получены
}
```
Строка `catch (e: CancellationException) { throw e }` обязательна в любом обобщённом обработчике ошибок в корутинах - без неё функция станет неотменяемой.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | исключение в `submit` → в `Future` | ошибка `launch` → родителю |
> | `Future.get()` → `ExecutionException` | `await()` → исходное исключение |
> | `CompletableFuture.exceptionally` | `try/catch` вокруг `await` |
> | `Thread.UncaughtExceptionHandler` | `CoroutineExceptionHandler` |
> | `InterruptedException` (проверяемое) | `CancellationException` (непроверяемое) |
> | ошибка задачи не влияет на пул | ошибка отменяет область |
> | нет аналога | `SupervisorJob` |
>
> **Ошибку в Java легко потерять молча.** `executor.submit(task)` складывает исключение в `Future`, и если `get()` никто не вызовет, об ошибке не узнает никто - ни логов, ни падения:
> ```java
> executor.submit(() -> { throw new RuntimeException("исчезнет"); });
> // тишина
> ```
> Корутины этого не допускают: ошибка `launch` обязательно дойдёт до родителя, а если никто не обработал - до `CoroutineExceptionHandler` или `Thread.uncaughtExceptionHandler`. Исключение одно - непрочитанный `async` в supervisor-области, и это считается ошибкой программиста.
>
> **`get()` оборачивает исключение, `await()` - нет.** В Java настоящая причина лежит внутри `ExecutionException`, поэтому обработка выглядит так:
> ```java
> try {
> future.get();
> } catch (ExecutionException e) {
> if (e.getCause() instanceof IOException io) { /* ... */ }
> }
> ```
> В Kotlin `await()` бросает то же исключение, что было выброшено внутри, поэтому `catch (e: IOException)` работает напрямую:
> ```kotlin
> import kotlinx.coroutines.*
> import java.io.IOException
>
> fun main() = runBlocking {
> val caught = try {
> coroutineScope { async { throw IOException("диск") }.await() }
> } catch (e: IOException) {
> "поймали ${e::class.simpleName}: ${e.message}"
> }
> println(caught) // поймали IOException: диск
> }
> ```
>
> **`exceptionally`/`handle` против `try/catch`.** В Java обработка ошибок в асинхронной цепочке - отдельный метод с лямбдой, и правила распространения ошибки по цепочке приходится помнить (`exceptionally` ловит, `thenApply` пропускает, `whenComplete` не меняет результат). В Kotlin работает обычный `try/catch`, с теми же правилами, что в синхронном коде.
>
> **Отмена пула против отмены области.** В Java ошибка в одной задаче никак не влияет на остальные - пул продолжает работу. Это и плюс (изоляция), и минус (бессмысленное продолжение работы, часть которой уже провалилась). Kotlin даёт выбор явно: `coroutineScope` - fail-fast, `supervisorScope` - изоляция как в Java.
>
> **`CancellationException` непроверяемое, `InterruptedException` - нет.** В Java прерывание приходится обрабатывать в каждом блокирующем вызове, и типичная ошибка - проглотить его, не восстановив флаг. В Kotlin отмену не нужно объявлять, но проглотить её так же легко через `catch (e: Exception)`.
>
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md), [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md)
> [!warning] Подводные камни
> **`try/catch` вокруг `launch` ничего не ловит:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> val job = launch(SupervisorJob()) {
> try {
> launch { error("внутри") }
> } catch (e: Exception) {
> println("не напечатается")
> }
> delay(50)
> }
> job.join()
> println("ошибка ушла наверх, а не в catch")
> // ошибка ушла наверх, а не в catch
> }
> ```
> `launch` возвращается до выполнения тела. Ловить нужно **внутри** лямбды или вокруг `coroutineScope`.
>
> **`catch (e: Exception)` перехватывает отмену:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> val job = launch {
> try {
> delay(1000)
> } catch (e: Exception) {
> println("отмена попала в общий catch: ${e is CancellationException}")
> }
> println("и работа продолжилась")
> }
> delay(20)
> job.cancelAndJoin()
> // отмена попала в общий catch: true
> // и работа продолжилась
> }
> ```
> Всегда ставьте `catch (e: CancellationException) { throw e }` первым.
>
> **`runCatching` в корутинах опасен** по той же причине - он ловит `Throwable`, включая `CancellationException`. В `suspend`-коде это делает блок неотменяемым, и `cancel()` перестаёт работать. Используйте явный `try/catch` с пробросом отмены.
>
> **`CoroutineExceptionHandler` в дочерней корутине игнорируется.** Он учитывается только у корневой корутины области. Передавать его нужно при создании scope или в корневой `launch`.
>
> **`CoroutineExceptionHandler` не работает с `async`** - его ошибка принадлежит `Deferred` и ждёт `await()`. Если `await()` не вызван в supervisor-области, ошибка исчезает навсегда.
>
> **`SupervisorJob`, переданный в `launch`, не делает детей независимыми:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> val job = launch(SupervisorJob()) { // supervisor — для ЭТОЙ корутины
> launch { delay(10); error("первый ребёнок") }
> launch {
> try { delay(1000) } finally { println("второго тоже отменили") }
> }
> }
> job.join()
> // второго тоже отменили
> }
> ```
> `SupervisorJob` в контексте билдера становится родителем самой корутины, но внутри её тела работает обычная область. Нужен `supervisorScope { }`.
>
> **`supervisorScope` не защищает код своего тела:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> val caught = runCatching {
> supervisorScope {
> launch { delay(1000) }
> error("ошибка в теле блока")
> }
> }.exceptionOrNull()?.message
> println(caught) // ошибка в теле блока
> }
> ```
> Supervisor меняет поведение только для детей.
>
> **Ошибка внутри `CoroutineExceptionHandler` роняет процесс.** Обработчик - последняя линия, выше его некуда. Логирование внутри него должно быть защищено от собственных сбоев.
>
> **`finally` может не выполниться при падении JVM.** Если необработанная ошибка дошла до `Thread.uncaughtExceptionHandler` и процесс завершается, часть блоков очистки в других корутинах может не успеть.
>
> **Подавленные исключения легко не заметить.** При нескольких одновременных ошибках `e.message` покажет только первую. В логах нужен полный `printStackTrace()` или `e.suppressed`.
>
> **Проброс через несколько уровней нельзя перехватить в середине.** Промежуточный `launch` не видит ошибку ребёнка как исключение - он просто отменяется. Чтобы обработать её на этом уровне, нужен `try/catch` вокруг `coroutineScope` внутри промежуточной корутины.
>
> **`CancellationException` как бизнес-ошибка не работает:**
> ```kotlin
> import kotlinx.coroutines.*
>
> class ValidationError(message: String) : CancellationException(message)
>
> fun main() = runBlocking {
> val job = launch(SupervisorJob()) { throw ValidationError("неверные данные") }
> job.join()
> println("ошибка проигнорирована: отменён=${job.isCancelled}")
> // ошибка проигнорирована: отменён=true
> }
> ```
> Наследование от `CancellationException` превращает исключение в «тихую отмену»: ни обработчика, ни падения. Для бизнес-ошибок наследуйтесь от `Exception`.
Далее: [Flow](Flow.md)