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

Что происходит при возникновении ошибки в корутине запущенной во viewModelScope

2.0 Middle🔥 171 комментариев
#Kotlin основы

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

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

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

Механизм обработки ошибок в корутинах viewModelScope

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

Базовые характеристики viewModelScope

Сначала важно понять свойства самой viewModelScope:

// Пример объявления viewModelScope в AndroidX Lifecycle
val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(JOB_KEY, 
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
    }

Ключевые особенности:

  • Использует SupervisorJob вместо обычного Job
  • По умолчанию привязана к Dispatchers.Main (с возможностью переопределения)
  • Автоматически отменяется при очистке ViewModel (onCleared())

Что происходит при исключении

  1. Поведение при использовании SupervisorJob Поскольку viewModelScope использует SupervisorJob, исключение в одной дочерней корутине не отменяет автоматически другие дочерние корутины. Это отличается от обычного Job, где исключение в дочерней корутине отменяет родителя и всех его детей.

    viewModelScope.launch {
        // Корутина 1 - если здесь выбросится исключение
        throw RuntimeException("Ошибка в корутине 1")
    }
    
    viewModelScope.launch {
        // Корутина 2 - продолжит работу даже при ошибке в корутине 1
        delay(1000)
        println("Эта корутина все еще работает")
    }
    
  2. Неотловленные исключения и CoroutineExceptionHandler Если исключение не перехвачено внутри корутины с помощью try/catch, оно передается родительской Job и обрабатывается через CoroutineExceptionHandler контекста.

    class MyViewModel : ViewModel() {
        init {
            viewModelScope.launch(
                CoroutineExceptionHandler { _, exception ->
                    // Обработка неотловленных исключений
                    println("Поймано исключение: ${exception.message}")
                    // В продакшене здесь можно отправить в Crashlytics/аналитику
                }
            ) {
                throw RuntimeException("Тестовая ошибка")
            }
        }
    }
    
  3. Эскалация исключений Если в контексте корутины нет CoroutineExceptionHandler, исключение эскалирует:

    • Для корутин, запущенных через launch - исключение передается родительской Job
    • Для корутин, запущенных через async и ожидаемых через await() - исключение выбрасывается в точке вызова await()
    // Пример с async/await
    viewModelScope.launch {
        try {
            val result = viewModelScope.async {
                throw RuntimeException("Ошибка в async")
            }.await() // Исключение выбросится здесь
        } catch (e: Exception) {
            // Перехватываем исключение
        }
    }
    
  4. Android-специфика: обработка в основном потоке Поскольку viewModelScope по умолчанию использует Dispatchers.Main, необработанные исключения могут привести к аварийному завершению приложения, если они происходят в основном потоке. Однако современные версии корутин предоставляют механизм отлова таких исключений до того, как они "уронят" приложение.

Практические последствия и рекомендации

Что происходит на практике:

  1. Без обработчика исключений - необработанное исключение обычно приводит к:

    • Завершению конкретной корутины с ошибкой
    • Логированию исключения в Logcat
    • В некоторых случаях - к аварийному завершению приложения (особенно для исключений в основном потоке)
  2. Влияние на другие корутины - благодаря SupervisorJob другие корутины в том же viewModelScope продолжают работу

Лучшие практики обработки ошибок:

class MyViewModel : ViewModel() {
    
    // Способ 1: Явная обработка с try/catch
    fun loadData() {
        viewModelScope.launch {
            try {
                val data = repository.loadData()
                // Обработка данных
            } catch (e: IOException) {
                // Обработка сетевых ошибок
                _errorState.value = e.message
            } catch (e: Exception) {
                // Общая обработка ошибок
                Log.e("ViewModel", "Ошибка загрузки", e)
            }
        }
    }
    
    // Способ 2: Использование CoroutineExceptionHandler
    private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        // Централизованная обработка ошибок
        when (exception) {
            is NetworkException -> handleNetworkError(exception)
            is DatabaseException -> handleDatabaseError(exception)
            else -> logToCrashReporting(exception)
        }
    }
    
    fun performOperation() {
        viewModelScope.launch(exceptionHandler) {
            // Операции, которые могут выбросить исключение
        }
    }
    
    // Способ 3: Использование sealed classes для обработки состояний
    sealed class Result<out T> {
        data class Success<T>(val data: T) : Result<T>()
        data class Error(val exception: Exception) : Result<Nothing>()
        object Loading : Result<Nothing>()
    }
}

Особые случаи

  1. Исключения в корутинах с жизненным циклом (lifecycleScope) В lifecycleScope также используется SupervisorJob, поэтому поведение схоже с viewModelScope.

  2. Взаимодействие с LiveData/StateFlow При использовании корутин для обновления LiveData или StateFlow рекомендуется обрабатывать ошибки внутри корутины и передавать состояния ошибки через сами данные:

    private val _uiState = MutableStateFlow<DataState>(DataState.Loading)
    val uiState: StateFlow<DataState> = _uiState
    
    fun loadData() {
        viewModelScope.launch {
            _uiState.value = DataState.Loading
            try {
                val data = repository.fetchData()
                _uiState.value = DataState.Success(data)
            } catch (e: Exception) {
                _uiState.value = DataState.Error(e.message ?: "Unknown error")
            }
        }
    }
    
  3. Отмена корутин при ошибке Несмотря на то, что SupervisorJob не отменяет все дочерние корутины, корутина, в которой произошло исключение, все равно завершается (отменяется).

Вывод

При возникновении ошибки в корутине viewModelScope:

  • Сама корутина завершается с ошибкой
  • Другие корутины продолжают работу благодаря SupervisorJob
  • Исключение либо обрабатывается через CoroutineExceptionHandler, либо эскалирует
  • При отсутствии обработчика может привести к аварийному завершению приложения
  • Рекомендуется всегда явно обрабатывать исключения или использовать централизованные обработчики ошибок

Наиболее надежный подход - комбинировать явную обработку try/catch для ожидаемых ошибок бизнес-логики и CoroutineExceptionHandler для неожиданных исключений, а также использовать реактивные потоки (StateFlow/LiveData) для передачи состояний ошибки в UI.

Что происходит при возникновении ошибки в корутине запущенной во viewModelScope | PrepBro