Files
obsidian/Kotlin/Корутины/Исключения в корутинах.md
T

530 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<br/>throw"] -->|"ошибка наверх"| C["Ребёнок уровня 2"]
C -->|"ошибка наверх"| B["Ребёнок уровня 1"]
B -->|"ошибка наверх"| A["Корень области"]
A -->|"отменить"| E["Соседняя ветка"]
A --> F{"есть обработчик?"}
F -->|"да"| G["CoroutineExceptionHandler"]
F -->|"нет"| H["Thread.uncaughtExceptionHandler<br/>падение"]
```
## Где 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<String> = supervisorScope {
val a = async { delay(20); "A" }
val b = async<String> { 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<Int> { 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 <T> 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)