1

Applying Kotlin Structured Concurrency: Part III — Exceptions in coroutines

 1 year ago
source link: https://proandroiddev.com/applying-kotlin-structured-concurrency-part-iii-exceptions-c5b043cc9a59
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Applying Kotlin Structured Concurrency: Part III — Exceptions in coroutines

In Java and Kotlin you can use try/catch for catch exceptions.

If you don’t handle an exception in a method where an exception was thrown then you need to handle it in the method that called this method and so on.

If there is no such handler in the callstack then user will get an app crash on Android.

Handling exceptions in coroutines work differently. In coroutines we have one more additional way for bubbling exception up — not only re-throw via callstack but also propagation via Job hierarchy. You can read more about Job hierarchy and Job parent-child relations in previous article:

Structured Concurrency for exception propagation uses the following machinery:

0. Exception is thrown in a child coroutine

  1. Parent cancels other child coroutines. That’s siblings cancellation
  2. Parent coroutine is cancelled too. That’s parent cancellation
  3. Exception is propagated up Job hierarchy
1*pz2Uzbmj-DqvVEbpKgzfPg.png

Different coroutine builders and suspend functions have differences in their behaviour. Let’s discuss them.

Top-level launch or launch in supervisorScope

Exception thrown here and not caught by try/catch inside launch OR propagated from child leads to:

  1. cancelling other siblings
  2. cancelling itself
  3. propagating up the Job hierarchy

These exceptions can’t be caught outside launch using try/catch because they will not be re-thrown via callstack but will be propagated via Job hierarchy.

Exception can be handled on top of Job hierarchy by installed CoroutineExceptionHandler or by inherited CoroutineExceptionHandler.

If CoroutineExceptionHandler is missing — the exception is passed to the thread’s uncaught exception handler. On Android, it will lead to application crash.

Top-level async or async in supervisorScope

Exception thrown here and not caught by try/catch inside async OR propagated from a child will be encapsulated in Deferredobject returned by the builder.

It is thrown as a normal exception only when invoking the await() method. Therefore — await() surrounded by try/catch should be used to avoid crash.

Without await() you can’t catch an exception in async surrounding by try/catch — so exception will be silently dropped.

CoroutineExceptionHandler has no effect here because there is no propagation via Job hierarchy.

Nested launch

Uncaught exception in launchOR exception propagated from child leads to:

  1. cancelling other children
  2. cancelling itself
  3. propagating up the Job hierarchy

These exceptions are not re-thrown but propagate up the Job hierarchy. If nested launch() is wrapped with try/catch then exception can’t be caught.

Nested async

Uncaught exception propagated from child OR occurred in async leads to:

  1. cancelling other children
  2. cancelling itself
  3. propagating up the Job hierarchy

Exception propagates up the Job hierarchy even without calling await() on it.

If await() is wrapped with try/catch then exception can be caught but it ANYWAY propagates via Job hierarchy.

coroutineScope

Uncaught exception in scoping function coroutineScopeOR exception propagated from child leads to:

  1. cancelling other children
  2. cancelling itself
  3. it doesn’t propagate exception up the Job hierarchy

Exception will be re-thrown, it allows to handle exception of failed coroutineScope surrounded by try/catch.

supervisorScope (exception thrown in scope)

Uncaught exception in scoping function supervisorScope (exception thrown in the block) leads to:

  1. cancelling other children
  2. cancelling itself
  3. it doesn’t propagate exceptions up the Job hierarchy.

Exception will be re-thrown which allows us to handle it by surrounding supervisorScope with try/catch.

supervisorScope (exception thrown in child coroutine)

If one of the child coroutines fails:

  1. the sibling coroutines are not cancelled
  2. supervisorScope is not cancelled
  3. no exception propagation up the Job hierarchy
  4. no re-throw exceptions
1*Bg8cjnfp4ZNcICd-t3hhvA.png

So surrounding supervisorScope with try/catch doesn’t catch exception and there is no propagation via Job hierarchy.

Coroutines that are started directly from the supervisorScope are top-level coroutines — behaviour of top level launch and async works like follows:

launch child coroutine requires installation of a CoroutineExceptionHandler (or using inherited CoroutineExceptionHandler from CoroutineExceptionHandler of parent Job). If there is no CoroutineExceptionHandler — exception is passed to the thread’s uncaught exception handler (crash on Android).

async/await requires surrounding await with try/catch. Without it — exception is passed to the thread’s uncaught exception handler (crash on Android).

Without await() exception will be silently dropped.

supervisorScope should be used when you have children those errors should propagate only up to a certain point and not all the way to the root.

withContext

Uncaught exception in scoping function withContext OR exception propagated from child leads to:

  1. cancelling other children
  2. cancelling itself
  3. it doesn’t propagate exceptions up the Job hierarchy.

Exception will be re-thrown, it allows to handle exception of failed withContext surrounded by try/catch.

CoroutineExceptionHandler

If you want to use cancellation functionality of Structured Concurrency then you shouldn’t use try/catch inside coroutines.

If an exception in a child coroutine should not stop other coroutines belonging to the same parent, or if you want to retry the operation, or if specific error-handling behaviour is required, try/catch blocks should be used inside coroutine.

Use the CoroutineExceptionHandler for logic that should happen after the coroutine has already been completed.

CoroutineExceptionHandler is a last-resort mechanism for global “catch all” behaviour. You cannot recover from the exception in the CoroutineExceptionHandler(scope is canceled and coroutine can’t be restarted). 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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK