Исключение не перехватывается в Coroutines

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

Вот моя установка:

Моя ViewModel запускает сопрограмму со своей областью видимости

class MyViewModel(private var myUseCase: MyUseCase) : ViewModel() {
    private val viewModelJob = Job()
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

    fun doSomething() {
        uiScope.launch {
            try {
                myUseCase()
            } catch (exception: Exception) {
                // Do error handling here
            }
        }
    }
}

Мой UseCase просто обрабатывает несколько логических операций, и в данном случае это какой-то валидатор.

class MyUseCase(private val myRepository: MyRepository) {
    suspend operator fun invoke() {
        if (checker()) {
            throw CustomException("Checker Failed due to: ...")
        }

        myRepository.doSomething()
    }
}

Тогда мой Repository просто обрабатывает сетевой уровень / локальный уровень

object MyRepository {
    private val api = ... // Retrofit

    suspend fun doSomething() = api.doSomething()
}

А вот и мой модифицированный интерфейс

interface MyInterface {
    @POST
    suspend fun doSomething()
}

Команда try/catch из ViewModel может обработать ошибку вызова Retrofit, но не может перехватить ошибку из CustomException, выброшенную UseCase. Из статей, которые я читал, это должно работать. Если я использую async, я могу сделать await и использовать ошибку, но мне не нужно использовать async в этом случае, и я обдумывал это. Я могу потеряться.

Любая помощь будет принята с благодарностью! Заранее спасибо!

Редактировать:

Вот журнал ошибок, который я получаю:

com.example.myapp.domain.errors.CustomException
        at com.example.myapp.domain.FeatureOne$invoke$2.invokeSuspend(FeatureOne.kt:34)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)

Ошибка напрямую указывает на явный оператор throw.

'org.jetbrains.kotlinx:kotlinx-coroutines-android' Вы используете этот артефакт? В основном это специфично для среды Android, поэтому в случае сбоя сопрограммы с необработанным исключением исключение регистрируется перед сбоем приложения Android.
Jeel Vankhede 22.05.2019 08:27

Ага. Я использую его вместе с последней версией. 1.2.1 и 1.3.30 для Котлина

Kurt Acosta 22.05.2019 08:32

Обновлен вопрос, чтобы показать трассировку стека

Kurt Acosta 22.05.2019 08:36
9
3
3 188
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

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

Элемент CoroutineExceptionHandlerконтекст используется как общий блок catch сопрограммы, где может выполняться обработка пользовательское ведение журнала или исключение. Это похоже на использование Thread.uncaughtExceptionHandler.

Как это использовать?

val handler = CoroutineExceptionHandler { _, exception -> 
    println("Caught $exception") 
}
val job = GlobalScope.launch(handler) {
    throw AssertionError()
}
val deferred = GlobalScope.async(handler) {
    throw ArithmeticException() // Nothing will be printed, relying on user to call 
    deferred.await()
}
joinAll(job, deferred)

В своем ViewModel убедитесь, что ваш uiScope использует SupervisorJob, а не Job. SupervisorJob может справиться с детской неудачей индивидуально. Job будет отменен, в отличие от SupervisorJob

Если вы используете 2.1.0 для AAC Lifecycle и ViewModel, вместо этого используйте расширение viewModelScope.

Я тоже пробовал! Но это спасает исключение. В следующий раз, когда он будет вызван, он просто вернет то же самое исключение без повторного выполнения блока запуска.

Kurt Acosta 22.05.2019 08:38

Может быть из-за вашего варианта использования? Вы все равно бросаете CustomException перед вызовом вызов репо. Возможно, вы каждый раз получаете исключение. Как насчет этого checker() метода?

Jeel Vankhede 22.05.2019 08:43

Предположительно, CustomException должен быть пойман блоком try/catch в ViewModel. Или я ошибаюсь? checker() — это просто своего рода валидатор, который я использую. Например, если ввод нулевой, я бросаю NoInputException или что-то в этом роде. Чего я пытаюсь добиться, так это распространить исключение на Activity и получить соответствующую строку из строковых ресурсов и показать диалог.

Kurt Acosta 22.05.2019 08:45

Нет, в том то и дело!! Блок Попробуйте поймать обычно не работает в сопрограммы таким образом. Вот почему нам нужно передать контекст как CoroutineExceptionHandler, чтобы он работал как Thread.uncaughtExceptionHandler.

Jeel Vankhede 22.05.2019 08:50

Да, в том-то и дело. Однако CoroutineExceptionHandler поймал это, как только исключение поймано, это результат каждый раз. Есть ли способ, чтобы он этого не делал?

Kurt Acosta 22.05.2019 08:52

Никогда не пробовал, так что не могу быть в этом уверен. Постараюсь скоро сообщить.

Jeel Vankhede 22.05.2019 08:55

Это работает, когда я использую GlobalScope, но терпит неудачу, если я использую область действия ViewModel, используя uiScope. Документы говорят, что if there is a Job in the context, then Job.cancel is invoked; Наверное, поэтому?

Kurt Acosta 22.05.2019 09:17

Думаю, это поможет: CoroutineScope(Dispatchers.Main + viewModelJob) для вашего uiScope.

Jeel Vankhede 22.05.2019 09:26

Спасибо! Это заставило меня приблизиться к проблеме. Я обновил код ViewModel выше. Я использовал Job, а не SupervisorJob. Изменил и теперь исправлено!

Kurt Acosta 22.05.2019 09:28

Просто для справки: try-catch вокруг suspend funявляется должен работать естественным образом и перехватывать любые исключения из вызываемого кода, и это все еще будет работать даже после приостановки и возобновления сопрограммы. Неудача ОП, скорее всего, связана с неправильным использованием async-await. Если задача async выдает исключение, это отменяет сопрограмму async, распространяя отмену на родительскую сопрограмму. SupervisorJob игнорирует отмену, но это обходной путь, а не решение.

Marko Topolnik 22.05.2019 14:18

Хорошо, @MarkoTopolnik, поэтому основная причина в том, что SupervisorJob обрабатывает Попробуйте поймать сам, не сообщая о конечной точке, откуда он был вызван.

Jeel Vankhede 22.05.2019 14:40

Нет, здесь действуют два различных механизма. Один — это отмена и его обнаружение, а другой — обработка исключений. Экземпляр Job связан с сопрограммой (она находится в ее контексте), но напрямую не участвует в ее выполнении. Должен быть явный код, который перехватывает исключения на верхнем уровне сопрограммы и вызывает coroutineContext.job.cancel() (этот код является внутренним для async). Реализация Job.cancel() по умолчанию, в свою очередь, вызывает parentJob.childCancelled(), и таким образом отмена достигает родителя SupervisorJob, чей childCancelled() не работает.

Marko Topolnik 22.05.2019 14:53

Теперь любой правильно написанный код не позволит необработанному исключению уйти от сопрограммы. Однако пользователи обычно неправильно используют async-await вместо withContext, неосознанно создавая подпрограмму без обработки исключений и предполагая, что try-catch вокруг deferred.await() они обрабатывают исключения внутри блока async. Но это просто обрабатывает исключение, с которым было завершено deferred. К тому времени async сопрограмма уже была отменена.

Marko Topolnik 22.05.2019 14:53

Насколько я знаю, Retrofit до сих пор не создал способ пометить методы ключевым словом suspend. Вы можете сослаться на этот связь. Таким образом, правильный способ вашего MyInterface будет:

interface MyInterface {
    @POST
    fun doSomething(): Deferred<Response<YourDataType>>
}

Я использую 2.5.1-SNAPSHOT version. Добавлена ​​первоклассная поддержка suspend. См. github.com/square/retrofit/pull/2886

Kurt Acosta 22.05.2019 09:26

Есть ли способ обрабатывать исключения, такие как тайм-аут, исключение сокета и т.д.

tinto mathew 18.12.2019 10:47

Да, подтвержденный ответ охватывает это

coroutineDispatcher 18.12.2019 10:55

Другой способ решить эту проблему — скрыть ваш пользовательский объект ошибки для реализации CancellationException

Например:

Ваш CustomException может быть реализован как:

sealed class CustomError : CancellationException() {
        data class CustomException(override val message: String = "Checker Failed due to: ...") : CustomError
}

Это исключение попадет в блок try/catch модели представления.

Другие вопросы по теме