TL;DR

Все ошибки перед попаданием UEH проходят через platformExceptionHandlers. На андроиде в platformExceptionHandlers добавляется CEH для обхода бага на андроиде 8. Если CEH из platformExceptionHandlers бросит ExceptionSuccessfullyProcessed, то ошибка не попадет в UEH.

В тексте будут использоваться следующие сокращения и термины:

UEH – uncaught exception handler. Сущность потока JVM. Предназначен для работы с необработанными ошибками. В обычной JVM по умолчанию ошибка пишется в консоль. В андроиде крэшится приложение. Место в исходном коде андроиде, где задается такое поведение:

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/os/RuntimeInit.java

CEH – coroutine exception handler. Сущность контекста корутины. Работает по аналогии с UEH, но на уровне корутины, а не на уровне потока. Так же служит для работы с необработанными ошибками.

Бросить ошибку – стандартное поведение в JVM, когда необработанная ошибка движется по стеку функций потока, пока не будет обработана в try-catch или UEH.

Распространить ошибку – поведение пришедшее из корутин. Когда в корутине попадается не обработанная ошибка, корутина отменяет себя и отправляется с ошибкой к родительской корутине.

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

Дисклеймер

Всё что угодно в этом материале, может быть враньём. Не принимайте решений на основании этого материала. Если всё-таки решите принять, то наймите профессионалов или перепроверяйте информацию самостоятельно

Если опираться на предыдущую статью (ссылка), то по отношению к корутине можно сказать что на андроиде она вызовет краш приложения, если не был переопределен UEH у потока. Потому что launch пойдёт с исключением к родителю (GlobalScope), а родитель не знает как обрабатывать исключения и отправит её для обработки обратно в launch. А launch при отбработке исключения отправляет ошибку в CEH, а при его отсутствии (как в текущем примере) отправляет в UEH.

Но во всех ли случаях такое будет? Или можно ли не изменяя корутину и не изменяя UEH потока помешать данной корутине закрыть приложение с крашем?

Для понимая как это можно сделать нужно сходить в исходный код корутин.

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

  • Потом сюда - StandaloneCoroutine создается при создании launch корутины. В ней переопределяется поведение что бы обрабатывать исключение не как в async.

  • Еще сюда - Тут происходит стандартная логика для launch, если есть CEH, то отправляем ошибку в него. В противном случае отправляем ее в UEH, что и будет происходить в handleUncaughtCoroutineException.

  • И вот сюда - А это место уже поможет найти ответ на выше заданные вопросы.

Функцию handleUncaughtCoroutineException можно разделить на две части. В нижней, под циклом for, ничего интересного не происходит. Мы идем в UEH потока и отправляем туда исключение, как говорилось выше. Но тогда может возникнуть вопрос, а зачем нужна вернхяя часть handleUncaughtCoroutineException, где находится цикл for, если вся работа происходит ниже? В цикле происходит проход по переменной platformExceptionHandlers, которая является коллекцией CEH в которые отправляются исключение. Тут может возникнуть еще один вопрос, а откуда берутся CEH в platformExceptionHandlers? И если подняться выше по классу, то можно увидеть что CEH в platformExceptionHandlers попадают из ServiceLoader.

Ну если на один вопрос, ответ получен, то вопрос, зачем нужна логика с platformExceptionHandlers, остается.

Для получения как минимум одной причины нужно сходить в класс AndroidExceptionPreHandler. Этот класс является реализацией CEH который прописан в META-INF и будет добавлен в platformExceptionHandlers при подключении библиотеки к проекту через ServiceLoader.

Зачем нужен AndroidExceptionPreHandler?

В android 8 немного изменилась логика работы с исключениями. До android 8, если у потока переопределить UEH, то система в этом случае не добавляла свой логики. Начиная с android 8 включительно, если у потока переопределить UEH, то помимо переопределенной логики система еще и залогирует исключение. И по этой причине на android 8 перед отправкой ошибки в UEH, в корутинах нужно вызвать функцию Thread.getUncaughtExceptionPreHandler, которой нет в оригинальном классе из JVM, поэтому это нужно делать через рефлексию. В android 9 это починили и выше указанных фикс через рефлексию уже не нужен.

https://developer.android.com/about/versions/oreo/android-8.0-changes#loue

И так получается, что, в корутинах перед отправкой в UEH, исключение в начале отправляется в коллекцию CEH полученные из ServiceLoader. Если вернуться к циклу for по коллекции platformExceptionHandlers, то можно увидеть что если CEH при этом бросит исключение ExceptionSuccessfullyProcessed, то функция handleUncaughtCoroutineException целиком завершится, и исключение не попадет в UEH.

Таким образом что бы при выполнении корутины из начала не было краша, нужно по аналогии с классом AndroidExceptionPreHandler создать экземпляр CEH и настроить его META-INF. Так же нужно что бы CEH бросил ExceptionSuccessfullyProcessed.

Для примера как можно создать и настроить такой CEH можно посмотреть здесь. Пример использования можно посмотреть здесь или подключить как зависимость ‘com.github.dracula6322:psychic-goggles:1.8.1.0’ через jitpack для корутин 1.8.1.

Почему в примерах используется java?

Т.к. ExceptionSuccessfullyProcessed имеет модификатор internal, поэтому из котлина его вызвать будет немного затруднительно. А из java кода это становится чуть проще.

Вывод: по итогу получился способ как на корутинах можно сделать глобальный CEH в который будут попадать исключения перед их обработки в UEH. Т.к. это настройка для CEH, то async исключения это не затронет. На текущий момент этот способ вряд ли можно назвать рекомендуемым, т.к. как минимум ExceptionSuccessfullyProcessed имеет модификатор internal. Поэтому вряд ли стоит закладываться бизнес логикой на этот способ. Как вариант тут можно обрабатывать (логировать или крашить приложение в тестинге) исключения которые были пропущены, что бы не нужно было везде прописывать для всех корутин CEH.