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

Какие знаешь нюансы обработки ошибок в Coroutines?

2.0 Middle🔥 131 комментариев
#Многопоточность и асинхронность

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

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

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

Нюансы обработки ошибок в Kotlin Coroutines

Обработка ошибок в корутинах — критически важный аспект, который имеет несколько ключевых нюансов, отличающих его от традиционной обработки исключений в потоках.

Различие между launch и async

Основное различие в поведении при ошибках между двумя билдерами корутин:

// launch - исключение немедленно выбрасывается и может быть обработано через try/catch в этой же корутине
val job = launch {
    try {
        throw RuntimeException("Ошибка в launch")
    } catch (e: Exception) {
        println("Поймано в launch: ${e.message}")
    }
}

// async - исключение откладывается до вызова await()
val deferred = async {
    throw RuntimeException("Ошибка в async")
}

launch {
    try {
        deferred.await()
    } catch (e: Exception) {
        println("Поймано при await: ${e.message}")
    }
}

Иерархия корутин и распространение ошибок

Structured Concurrency (структурная конкурентность) означает, что исключения распространяются вверх по иерархии корутин:

val scope = CoroutineScope(SupervisorJob()) // Или Job() для разных стратегий

scope.launch {
    launch {
        throw RuntimeException("Дочерняя корутина упала")
    }
    delay(1000)
    println("Эта строка не выполнится при использовании обычного Job")
}

SupervisorJob и SupervisorScope

SupervisorJob — специальный тип Job, который не отменяет родительскую корутину и соседние дочерние корутины при ошибке в одной из них:

// С SupervisorJob ошибка в одной корутине не отменяет другие
val supervisorScope = CoroutineScope(SupervisorJob())

supervisorScope.launch {
    delay(100)
    throw RuntimeException("Ошибка 1")
}

supervisorScope.launch {
    delay(200)
    println("Эта корутина выполнится, несмотря на ошибку в первой")
}

// Альтернатива - использование supervisorScope
supervisorScope {
    launch {
        throw RuntimeException("Ошибка в supervisorScope")
    }
    launch {
        delay(500)
        println("Эта корутина продолжит работу")
    }
}

CoroutineExceptionHandler

CoroutineExceptionHandler — глобальный обработчик для корутин, который ловит непойманные исключения. Важные нюансы:

  1. Работает только с корутинами, запущенными через launch
  2. Не работает с async (исключения остаются в Deferred)
  3. Должен быть установлен в корневом скоупе
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
    println("Глобальный обработчик поймал: ${throwable.message}")
}

val scope = CoroutineScope(Job() + exceptionHandler)

scope.launch {
    throw RuntimeException("Исключение для глобального обработчика")
}

Отмена и исключения

При отмене корутины выбрасывается CancellationException, который должен обрабатываться особым образом:

val job = launch {
    try {
        delay(1000)
    } catch (e: CancellationException) {
        println("Корутина была отменена")
        throw e // Обязательно пробрасываем дальше
    }
}

delay(100)
job.cancel()

Опасности при комбинировании контекстов

Неправильное комбинирование контекстов может привести к неожиданному поведению:

// ОШИБКА: ExceptionHandler не сработает, так как SupervisorJob перехватывает исключения
CoroutineScope(SupervisorJob() + exceptionHandler).launch {
    launch {
        throw RuntimeException("Исключение потеряно!")
    }
}

Лучшие практики

  1. Всегда обрабатывайте исключения в корутинах — непойманные исключения приведут к краху приложения
  2. Используйте SupervisorScope для независимых операций — когда неудача одной операции не должна влиять на другие
  3. Для async используйте try-catch вокруг await() или обрабатывайте исключения внутри самой асинхронной корутины
  4. Помните о различиях между launch и async при проектировании обработки ошибок
  5. Используйте CoroutineExceptionHandler для логирования глобальных ошибок, но не для восстановления
  6. Всегда пробрасывайте CancellationException — это важно для корректной работы механизма отмены
// Рекомендуемый подход для сложных операций
suspend fun safeOperation(): Result<Data> = try {
    val data = riskyOperation()
    Result.success(data)
} catch (e: Exception) {
    Result.failure(e)
}

// Или использование встроенных механизмов
val result = runCatching {
    riskyOperation()
}

Понимание этих нюансов критически важно для создания стабильных асинхронных приложений на Kotlin, где ошибки обрабатываются предсказуемо и не приводят к неожиданному поведению всей системы.

Какие знаешь нюансы обработки ошибок в Coroutines? | PrepBro