Android coroutine exception handler

Coroutine Exception Handler

An optional element in the coroutine context to handle uncaught exceptions.

Normally, uncaught exceptions can only result from root coroutines created using the launch builder. All children coroutines (coroutines created in the context of another Job) delegate handling of their exceptions to their parent coroutine, which also delegates to the parent, and so on until the root, so the CoroutineExceptionHandler installed in their context is never used. Coroutines running with SupervisorJob do not propagate exceptions to their parent and are treated like root coroutines. A coroutine that was created using async always catches all its exceptions and represents them in the resulting Deferred object, so it cannot result in uncaught exceptions.

Handling coroutine exceptions

CoroutineExceptionHandler is a last-resort mechanism for global «catch all» behavior. You cannot recover from the exception in the CoroutineExceptionHandler . The coroutine had already completed with the corresponding exception when the handler is called. Normally, the handler is used to log the exception, show some kind of error message, terminate, and/or restart the application.

If you need to handle exception in a specific part of the code, it is recommended to use try / catch around the corresponding code inside your coroutine. This way you can prevent completion of the coroutine with the exception (exception is now caught), retry the operation, and/or take other arbitrary actions:

Implementation details

By default, when no handler is installed, uncaught exception are handled in the following way:

If exception is CancellationException then it is ignored (because that is the supposed mechanism to cancel the running coroutine)

if there is a Job in the context, then Job.cancel is invoked;

Otherwise, all instances of CoroutineExceptionHandler found via ServiceLoader

and current thread’s Thread.uncaughtExceptionHandler are invoked.

CoroutineExceptionHandler can be invoked from an arbitrary thread.

Источник

Coroutine exceptions handling

This section covers exception handling and cancellation on exceptions. We already know that a cancelled coroutine throws CancellationException in suspension points and that it is ignored by the coroutines’ machinery. Here we look at what happens if an exception is thrown during cancellation or multiple children of the same coroutine throw an exception.

Exception propagation

Coroutine builders come in two flavors: propagating exceptions automatically (launch and actor) or exposing them to users (async and produce). When these builders are used to create a root coroutine, that is not a child of another coroutine, the former builders treat exceptions as uncaught exceptions, similar to Java’s Thread.uncaughtExceptionHandler , while the latter are relying on the user to consume the final exception, for example via await or receive (produce and receive are covered in Channels section).

It can be demonstrated by a simple example that creates root coroutines using the GlobalScope:

GlobalScope is a delicate API that can backfire in non-trivial ways. Creating a root coroutine for the whole application is one of the rare legitimate uses for GlobalScope , so you must explicitly opt-in into using GlobalScope with @OptIn(DelicateCoroutinesApi::class) .

You can get the full code here.

The output of this code is (with debug):

CoroutineExceptionHandler

It is possible to customize the default behavior of printing uncaught exceptions to the console. CoroutineExceptionHandler context element on a root coroutine can be used as a generic catch block for this root coroutine and all its children where custom exception handling may take place. It is similar to Thread.uncaughtExceptionHandler . You cannot recover from the exception in the CoroutineExceptionHandler . The coroutine had already completed with the corresponding exception when the handler is called. Normally, the handler is used to log the exception, show some kind of error message, terminate, and/or restart the application.

On JVM it is possible to redefine global exception handler for all coroutines by registering CoroutineExceptionHandler via ServiceLoader . Global exception handler is similar to Thread.defaultUncaughtExceptionHandler which is used when no more specific handlers are registered. On Android, uncaughtExceptionPreHandler is installed as a global coroutine exception handler.

Читайте также:  Custom style android studio

CoroutineExceptionHandler is invoked only on uncaught exceptions — exceptions that were not handled in any other way. In particular, all children coroutines (coroutines created in the context of another Job) delegate handling of their exceptions to their parent coroutine, which also delegates to the parent, and so on until the root, so the CoroutineExceptionHandler installed in their context is never used. In addition to that, async builder always catches all exceptions and represents them in the resulting Deferred object, so its CoroutineExceptionHandler has no effect either.

Coroutines running in supervision scope do not propagate exceptions to their parent and are excluded from this rule. A further Supervision section of this document gives more details.

You can get the full code here.

The output of this code is:

Cancellation and exceptions

Cancellation is closely related to exceptions. Coroutines internally use CancellationException for cancellation, these exceptions are ignored by all handlers, so they should be used only as the source of additional debug information, which can be obtained by catch block. When a coroutine is cancelled using Job.cancel, it terminates, but it does not cancel its parent.

You can get the full code here.

The output of this code is:

If a coroutine encounters an exception other than CancellationException , it cancels its parent with that exception. This behaviour cannot be overridden and is used to provide stable coroutines hierarchies for structured concurrency. CoroutineExceptionHandler implementation is not used for child coroutines.

In these examples, CoroutineExceptionHandler is always installed to a coroutine that is created in GlobalScope. It does not make sense to install an exception handler to a coroutine that is launched in the scope of the main runBlocking, since the main coroutine is going to be always cancelled when its child completes with exception despite the installed handler.

The original exception is handled by the parent only when all its children terminate, which is demonstrated by the following example.

You can get the full code here.

The output of this code is:

Exceptions aggregation

When multiple children of a coroutine fail with an exception, the general rule is «the first exception wins», so the first exception gets handled. All additional exceptions that happen after the first one are attached to the first exception as suppressed ones.

You can get the full code here.

Note: This above code will work properly only on JDK7+ that supports suppressed exceptions

The output of this code is:

Note that this mechanism currently only works on Java version 1.7+. The JS and Native restrictions are temporary and will be lifted in the future.

Cancellation exceptions are transparent and are unwrapped by default:

You can get the full code here.

The output of this code is:

Supervision

As we have studied before, cancellation is a bidirectional relationship propagating through the whole hierarchy of coroutines. Let us take a look at the case when unidirectional cancellation is required.

A good example of such a requirement is a UI component with the job defined in its scope. If any of the UI’s child tasks have failed, it is not always necessary to cancel (effectively kill) the whole UI component, but if the UI component is destroyed (and its job is cancelled), then it is necessary to cancel all child jobs as their results are no longer needed.

Another example is a server process that spawns multiple child jobs and needs to supervise their execution, tracking their failures and only restarting the failed ones.

Supervision job

The SupervisorJob can be used for these purposes. It is similar to a regular Job with the only exception that cancellation is propagated only downwards. This can easily be demonstrated using the following example:

You can get the full code here.

The output of this code is:

Supervision scope

Instead of coroutineScope, we can use supervisorScope for scoped concurrency. It propagates the cancellation in one direction only and cancels all its children only if it failed itself. It also waits for all children before completion just like coroutineScope does.

Читайте также:  Идеальная камера для андроид

You can get the full code here.

The output of this code is:

Exceptions in supervised coroutines

Another crucial difference between regular and supervisor jobs is exception handling. Every child should handle its exceptions by itself via the exception handling mechanism. This difference comes from the fact that child’s failure does not propagate to the parent. It means that coroutines launched directly inside the supervisorScope do use the CoroutineExceptionHandler that is installed in their scope in the same way as root coroutines do (see the CoroutineExceptionHandler section for details).

Источник

Корутины: исключения

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

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

Если вы предпочитаете видео, посмотрите это обсуждение KotlinConf’19 от Флорины Мунтенеску и меня (англ):

⚠️ Для того, чтобы понимать остальную часть статьи без каких-либо проблем, необходимо прочитать и понять часть 1 серии.

Корутина внезапно завершилась! И что теперь? ?

Когда корутина терпит неудачу с исключением, она распространит его до своего родителя! Затем родитель 1) отменит остальные свои дочерние элементы, 2) отменит себя и 3) распространит исключение до своего родителя.

Исключение достигнет корня иерархии, и все сопрограммы, которые запустил CoroutineScope , также будут отменены.

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

А что, если вы не хотите такого поведения? Кроме того, возможно использовать другую реализацию Job , а именно SupervisorJob , в CoroutineContext CoroutineScope который и создает эти корутины.

SupervisorJob на помощь

С SupervisorJob неудача одного дочернего элемента не влияет на других. SupervisorJob не отменит ни саму себя, ни остальных своих “детей”. Кроме того, SupervisorJob также не будет распространять исключение и позволит дочерней корутине обрабатывать его.

Вы можете создать CoroutineScope , подобный этому val uiScope = CoroutineScope(SupervisorJob()) , чтобы не распространять отмену при сбое корутины, как показано на этом изображении:

Если исключение не обрабатывается и у CoroutineContext нет обработчика CoroutineExceptionHandler (как мы увидим позже), то оно достигнет ExceptionHandler потока по умолчанию. В JVM исключение будет регистрироваться в консоли, а в Android оно приведет к сбою вашего приложения независимо от Диспетчера, на котором это происходит.

?Неотловленные исключения всегда будут отбрасываться независимо от того, какой тип Job вы используете

То же самое поведение применяется к конструкторам областей CoroutineScope и supervisorScope . Они создадут подпространство (с Job или SupervisorJob соответственно в качестве родителя), с помощью которого вы можете логически сгруппировать корутины (например, если вам надо выполнять параллельные вычисления или же вы хотите, чтобы они были или не были затронуты друг другом).

Предупреждение: SupervisorJob работает только так, как описано, являясь при этом частью области видимости: то есть создано с помощью supervisorScope , либо CoroutineScope(SupervisorJob()) .

Job или SupervisorJob ? ?

Когда вы должны использовать Job или SupervisorJob ? Используйте SupervisorJob или supervisorScope , если вы не хотите, чтобы сбой отменял родителя и его сиблингов.

В этом случае, если child#1 не сработает, ни область действия, ни child#2 не будут отменены. Еще пример:

В этом случае, поскольку supervisorScope создает подпространство с заданием SupervisorJob , если child#1 завершится неудачей, child#2 не будет отменен. Если вместо этого вы используете CoroutineScope , сбой будет распространяться дальше и в конечном итоге приведет к отмене области.

Читайте также:  Как удалить родительский контроль андроид

Итак, викторина! А кто же мой родитель? ?

Учитывая следующий фрагмент кода, можете ли вы определить, какой тип Job child#1 имеет в качестве родителя?

parentJob child#1 — это работа типа Job ! Надеюсь, все понятно! Даже если на первый взгляд можно подумать, что это может быть SupervisorJob , это не потому, что новой корутине всегда назначается новое Job() , которое в данном случае переопределяет SupervisorJob . SupervisorJob является родителем корутины, созданной с помощью scope.launch , так что SupervisorJob ничего не делает в этом коде!

Таким образом, если child#1 или child#2 завершится неудачно, то ошибка достигнет области действия и вся работа, начатая ей, будет отменена.

Помните, что SupervisorJob работает только так, как он описан, являясь при этом частью области: то есть создан с помощью supervisorScope , либо CoroutineScope(SupervisorJob()) . Передача SupervisorJob в качестве параметра конструктора корутины не будет иметь желаемого эффекта, который можно ожидать от отмены.

Что касается исключений, то если какой-либо дочерний элемент создает исключение, то SupervisorJob не будет распространять его вверх по иерархии и позволит корутине обработать его.

Под капотом

Если вам интересно, как работает Job под капотом, ознакомьтесь с реализацией функций childCancelled и notifyCancelling в файле JobSupport.kt .

В реализации SupervisorJob метод childCancelled просто возвращает false , что означает, что он не распространяет отмену, но и не обрабатывает исключение.

Работа с исключениями ? ?

Корутины используют обычный синтаксис Kotlin для обработки исключений: try/catch либо встроенные вспомогательные функции, такие как runCatching ( который использует try/catch внутренне).

Ранее упоминалось, что всегда будут появляться неучтенные исключения. Однако разные сборщики корутин трактуют исключения по-разному.

Запуск

При запуске исключения будут появляться сразу же, как только они произойдут. Таким образом, вы можете обернуть код, который может создавать исключения внутри try/catch , как в этом примере:

Async

Когда async используется в качестве корневой корутины (являющейся прямым потомком экземпляра CoroutineScope или supervisorScope ), исключения создаются не автоматически, а при вызове .await() .

Чтобы обрабатывать исключения, создаваемые async всякий раз корневой корутиной, можно обернуть ее .await() вызова внутри try/catch :

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

Также обратите внимание, что мы используем supervisorScope для вызова async и await . Как мы уже говорили, SupervisorJob позволяет корутине обрабатывать исключение, в отличие от Job , которое автоматически распространит его вверх по иерархии, поэтому блок catch не будет вызван:

Кроме того, исключения, которые происходят в корутинах, созданных другими, всегда будут распространяться независимо от их конструктора. Например:

В этом случае, если async создает исключение, оно будет вызвано сразу же, потому что launch — это корутина, являющаяся прямым дочерним элементом области. Причина заключается в том, что async (с Job в его CoroutineContext ) автоматически распространит исключение до его родителя ( launch ), который его и вызовет.

⚠️Исключения, вызванные в конструкторе CoroutineScope или в корутинах, созданных другими, не будут пойманы в try/catch !

В разделе SupervisorJob мы упоминаем о существовании CoroutineExceptionHandler . Давайте погрузимся в него!

CoroutineExceptionHandler

CoroutineExceptionHandler — это необязательный элемент CoroutineContext , позволяющий обрабатывать неучтенные исключения.

Вот как вы можете определить CoroutineExceptionHandler : всякий раз, когда ловится исключение, у вас есть информация о CoroutineContext , где оно произошло, а также непосредственно само исключение:

Исключения будут пойманы, если требования будут выполнены:

  • Когда⏰: исключение создается корутиной, которая автоматически создает другие исключения (работает с launch , а не с async ).
  • Где ?: если он находится в CoroutineContext CoroutineScope или корневой корутине (прямой потомок CoroutineScope или supervisorScope ).

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

В другом случае, когда обработчик установлен во внутренней корутине, он не будет пойман:

Исключение не перехватывается, поскольку обработчик не установлен в правильном CoroutineContext . Внутренний запуск распространит его до родителя и , поскольку он ничего не знает об обработчике, вызовется другое исключение.

Изящно справляясь с исключениями в вашем приложении, вы сможете предоставлять хороший пользовательский опыт, даже если все идет не так, как ожидалось.

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

Неучтенные исключения будут увеличиваться в числе. Отлавливайте их, чтобы обеспечить отличный UX!

Источник

Оцените статью