Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Why exception handling with Kotlin Coroutines is hard and how to successfully master it!

Why exception handling with Kotlin Coroutines is hard and how to successfully master it!

Getting the “happy path” right with Kotlin Coroutines is pretty straightforward. On the other hand, handling exceptions appropriately when something goes wrong is not that trivial.

In this talk, you will learn about the reasons for the complexity of exception handling with Kotlin Coroutines and about all things you need to consider to appropriately deal with occurring exceptions.
You will learn when you should use a conventional try-catch clause, and when you should install a CoroutineExceptionHandler instead.
You will also learn about the special properties of top-level Coroutines when it comes to exception handling and how they differ when they are starting with launch and async.
Furthermore, you will learn all about the exception handling peculiarities of the scoping functions coroutineScope{} and supervisorScope{} and why you should re-throw CancellationExceptions to avoid subtle errors.

This talk is for developers that are already familiar with the basics of Kotlin Coroutines but still struggle to understand how exception handling works in detail. By the end of this talk, you will have a better understanding of how exceptions are treated by the Coroutines machinery and how you can handle them appropriately.

Lukas Lechner

October 09, 2020
Tweet

More Decks by Lukas Lechner

Other Decks in Programming

Transcript

  1. Why exception handling with Kotlin Coroutines is hard and how

    to master it! @LukasLechnerDev www.lukaslechner.com
  2. fun main() { // some code throw Exception() } >_

    Exception in thread "main" java.lang.Exception @LukasLechnerDev
  3. fun main() { try { // some code throw Exception()

    } catch (exception: Exception) { println(“Handled exception") } } >_ Handled exception fun main() { try { // some code throw Exception() } catch (exception: Exception) { println(“Handled exception") } } @LukasLechnerDev
  4. fun main() { try { functionThatThrows() } catch (exception: Exception)

    { println(“Handled exception") } } fun functionThatThrows() { // some code throw Exception() } >_ Handled exception fun main() { try { functionThatThrows() } catch (exception: Exception) { println(“Handled exception") } } fun functionThatThrows() { // some code throw Exception() } @LukasLechnerDev
  5. fun main() { val rootScope = CoroutineScope(Job()) rootScope.launch { throw

    Exception() } Thread.sleep(100) } >_ Exception in thread "DefaultDispatcher-worker-1" java.lang.Exception
  6. fun main() { val rootScope = CoroutineScope(Job()) rootScope.launch { try

    { throw Exception() } catch (exception: Exception) { println(“Handled $exception") } } Thread.sleep(100) } >_ Handled exception fun main() { val rootScope = CoroutineScope(Job()) rootScope.launch { try { throw Exception() } catch (exception: Exception) { println(“Handled $exception") } } Thread.sleep(100) }
  7. ... rootScope.launch { try { suspendFunctionThatThrows() } catch (exception: Exception)

    { println(“Handled $exception") } } suspend fun suspendFunctionThatThrows() { throw Exception() } >_ Handled exception ... rootScope.launch { try { suspendFunctionThatThrows() } catch (exception: Exception) { println(“Handled $exception") } } suspend fun suspendFunctionThatThrows() { throw Exception() }
  8. ... rootScope.launch { try { throw Exception() } catch (exception:

    Exception) { println("Handled $exception") } } }
  9. ... rootScope.launch { try { launch { throw Exception() }

    } catch (exception: Exception) { println("Handle $exception") } } >_ Exception in thread “DefaultDispatcher-worker-2" java.lang.Exception ... rootScope.launch { try { launch { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } }
  10. ... val rootScope = CoroutineScope(Job()) rootScope.launch { try { launch

    { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } }
  11. ... val rootScope = CoroutineScope(Job()) rootScope.launch { try { launch

    { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } rootScope Job
  12. ... val rootScope = CoroutineScope(Job()) rootScope.launch { try { launch

    { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } Job Coroutine started with launch{} rootScope Job
  13. Job Child Coroutine started with launch{} ... val rootScope =

    CoroutineScope(Job()) rootScope.launch { try { launch { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } Job Coroutine started with launch{} rootScope Job
  14. Uncaught Exception Handler Job Child Coroutine started with launch{} ...

    val rootScope = CoroutineScope(Job()) rootScope.launch { try { launch { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } Job Coroutine started with launch{} rootScope Job
  15. val ceh = CoroutineExceptionHandler {…} val rootScope = CoroutineScope(Job()) rootScope.launch

    { try { launch { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } Child Coroutine Root Coroutine rootScope
  16. val ceh = CoroutineExceptionHandler {…} val rootScope = CoroutineScope(Job()) rootScope.launch

    { try { launch { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } Child Coroutine Root Coroutine rootScope
  17. Uncaught Exception Handler val ceh = CoroutineExceptionHandler {…} val rootScope

    = CoroutineScope(Job()) rootScope.launch { try { launch(ceh) { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } Child Coroutine Root Coroutine rootScope Coroutine Exception Handler val ceh = CoroutineExceptionHandler {…} val rootScope = CoroutineScope(Job()) rootScope.launch { try { launch(ceh) { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } >_ Exception in thread “DefaultDispatcher- worker-2" java.lang.Exception
  18. val ceh = CoroutineExceptionHandler {…} val topLevelScope = CoroutineScope(Job()) topLevelScope.launch(ceh)

    { try { launch { throw Exception() } } catch (exception: Exception) { println("Handle $exception") } } val ceh = CoroutineExceptionHandler {…} val rootScope = CoroutineScope(Job()) rootScope.launch(ceh) { try { launch { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } Child Coroutine Root Coroutine rootScope Coroutine Exception Handler
  19. val ceh = CoroutineExceptionHandler {…} val topLevelScope = CoroutineScope(Job()) topLevelScope.launch(ceh)

    { try { launch { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } val ceh = CoroutineExceptionHandler {…} val rootScope = CoroutineScope(Job()) rootScope.launch(ceh) { try { launch { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } Child Coroutine Root Coroutine rootScope Coroutine Exception Handler >_ Handled exception in CoroutineExceptionHandler
  20. val ceh = CoroutineExceptionHandler {…} val rootScope = CoroutineScope(Job() +

    ceh) rootScope.launch { try { launch { throw Exception() } } catch (exception: Exception) { println("Handled $exception") } } Child Coroutine Root Coroutine rootScope Coroutine Exception Handler
  21. val ceh = CoroutineExceptionHandler {…} val rootScope = CoroutineScope(Job() +

    ceh) rootScope.launch { try { launch { throw Exception() } } catch (exception: Exception) { println("Handled $exception") } } Child Coroutine Root Coroutine rootScope Coroutine Exception Handler
  22. val ceh = CoroutineExceptionHandler {…} val rootScope = CoroutineScope(Job() +

    ceh) rootScope.launch { try { launch { throw Exception() } } catch (exception: Exception) { println("Handled $exception") } } Child Coroutine Root Coroutine rootScope Coroutine Exception Handler Coroutine Exception Handler >_ Handled exception in CoroutineExceptionHandler
  23. Coroutine Exception Handler try/catch ➡ Global “catch all” behaviour ➡

    Coroutines have already completed => no recovery possible ➡ e.g. log exception, show error message ➡ handle the exception directly in the Coroutine ➡ possibility to recover ➡ e.g. retry operation or perform other arbitrary operations
  24. rootScope.launch { val result1 = async { networkRequest1() } val

    result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  25. rootScope.launch { val result1 = async { networkRequest1() } val

    result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  26. rootScope.launch { val result1 = async { networkRequest1() } val

    result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  27. rootScope.launch { val result1 = async { networkRequest1() } val

    result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  28. rootScope.launch { val result1 = async { networkRequest1() } val

    result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  29. rootScope.launch { val result1 = async { networkRequest1() } val

    result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  30. rootScope.launch { val result1 = async { networkRequest1() } val

    result2 = async { networkRequest2() } display(awaitAll(result1, result2)) } rootScope
  31. rootScope.launch { val result1 = async { networkRequest1() } val

    result2 = async { networkRequest2() } display(awaitAll(result1, result2)) } launch rootScope
  32. rootScope.launch { val result1 = async { networkRequest1() } val

    result2 = async { networkRequest2() } display(awaitAll(result1, result2)) } async launch rootScope async
  33. launch async async rootScope rootScope.launch { val result1 = async

    { networkRequest1() } val result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  34. launch async async rootScope rootScope.launch { val result1 = async

    { networkRequest1() } val result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  35. launch async async rootScope rootScope.launch { val result1 = async

    { try { networkRequest1() } catch (e: Exception) { // Log exception null } } val result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  36. launch async async rootScope rootScope.launch { val result1 = async

    { try { networkRequest1() } catch (e: Exception) { // Log exception null } } val result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  37. rootScope launch async async rootScope.launch { val result1 = async

    { try { networkRequest1() } catch (e: Exception) { // Log exception null } } val result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  38. rootScope launch async async rootScope.launch { val result1 = async

    { try { networkRequest1() } catch (e: Exception) { // Log exception null } } val result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  39. rootScope.launch { val result1 = async { networkRequest1() } val

    result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  40. val ceh = CoroutineExceptionHandler {…} rootScope.launch { val result1 =

    async { networkRequest1() } val result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  41. val ceh = CoroutineExceptionHandler {…} rootScope.launch(ceh) { val result1 =

    async { networkRequest1() } val result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  42. val ceh = CoroutineExceptionHandler {…} rootScope.launch(ceh) { val result1 =

    async { networkRequest1() } val result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  43. val job = launch { println("Starting network request") delay(1000) println("Coroutine

    still running ... ") } delay(500) job.cancel() throws CancellationException after 500ms •>_ Starting network request
  44. val job = launch { try { println("Starting network request")

    delay(1000) } catch (e: Exception) { println("Caught: $e") } println("Coroutine still running ... ") } delay(500) job.cancel()
  45. val job = launch { try { println("Starting network request")

    delay(1000) } catch (e: Exception) { println("Caught: $e") } println("Coroutine still running ... ") } delay(500) job.cancel() •>_ Starting network request • Caught: JobCancellationException • Coroutine still running ...
  46. val job = launch { try { println("Starting network request")

    delay(1000) } catch (e: HttpException) { println("Caught: $e") } println("Coroutine still running ... ") } delay(500) job.cancel()
  47. val job = launch { try { println("Starting network request")

    delay(1000) } catch (e: Exception) { if (e is CancellationException) { throw e } } println("Coroutine still running ... ") } delay(500) job.cancel()
  48. fun main() { val rootScope = CoroutineScope(Job()) rootScope.async { throw

    Exception() } Thread.sleep(100) } •>_ No Output
  49. ... val deferredResult = rootScope.async { throw Exception() } rootScope.launch

    { try { deferredResult.await() } catch (exception: Exception) { println(“Handled $exception") } }
  50. ... val deferredResult = rootScope.async { throw Exception() } rootScope.launch

    { try { deferredResult.await() } catch (exception: Exception) { println(“Handled $exception") } }
  51. ... val deferredResult = rootScope.async { throw Exception() } val

    ceh = CoroutineExceptionHandler { ... } rootScope.launch(ceh) { deferredResult.await() }
  52. ... val deferredResult = rootScope.async { throw Exception() } val

    ceh = CoroutineExceptionHandler { ... } rootScope.launch(ceh) { deferredResult.await() }
  53. ... val rootScope = CoroutineScope(Job()) rootScope.launch { async { throw

    Exception() } } async{} child Coroutine launch{} root Coroutine rootScope >_ Exception in thread “DefaultDispatcher- worker-2" java.lang.Exception
  54. ... rootScope.launch { try { launch { throw Exception() }

    } catch (exception: Exception) { println(“Handled $exception") } }
  55. ... rootScope.launch { try { launch { throw Exception() }

    } catch (exception: Exception) { println(“Handled $exception") } }
  56. ... rootScope.launch { try { launch { throw Exception() }

    } catch (exception: Exception) { println(“Handled $exception") } }
  57. >_ Exception in thread “DefaultDispatcher-worker-2" java.lang.Exception ... rootScope.launch { try

    { launch { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } }
  58. rootScope.launch { try { coroutineScope { launch { throw Exception()

    } } } catch (exception: Exception) { println(“Handled $exception") } }
  59. rootScope.launch { try { coroutineScope { launch { throw Exception()

    } } } catch (exception: Exception) { println(“Handled $exception") } } >_ Handled exception
  60. try { suspendFunction() } catch (exception: Exception) { println(“Handled $exception")

    } suspend fun suspendFunction() = coroutineScope { // suspend function body }
  61. try { suspendFunction() } catch (exception: Exception) { println(“Handled $exception")

    } suspend fun suspendFunction() = coroutineScope { // suspend function body }
  62. rootScope.launch { // Coroutine 1 launch { // Coroutine 2

    } supervisorScope { launch { // Coroutine 3 } launch { // Coroutine 4 } } }
  63. rootScope.launch { // Coroutine 1 launch { // Coroutine 2

    } supervisorScope { launch { // Coroutine 3 } launch { // Coroutine 4 } } } rootScope
  64. rootScope.launch { // Coroutine 1 launch { // Coroutine 2

    } supervisorScope { launch { // Coroutine 3 } launch { // Coroutine 4 } } } Coroutine 1 rootScope
  65. rootScope.launch { // Coroutine 1 launch { // Coroutine 2

    } supervisorScope { launch { // Coroutine 3 } launch { // Coroutine 4 } } } Coroutine 2 Coroutine 1 rootScope
  66. rootScope.launch { // Coroutine 1 launch { // Coroutine 2

    } supervisorScope { launch { // Coroutine 3 } launch { // Coroutine 4 } } } Coroutine 2 SupervisorJob Coroutine 1 rootScope
  67. rootScope.launch { // Coroutine 1 launch { // Coroutine 2

    } supervisorScope { launch { // Coroutine 3 } launch { // Coroutine 4 } } } Coroutine 2 Coroutine 3 SupervisorJob Coroutine 4 Coroutine 1 rootScope supervisorScope
  68. Recap 1. If an exception is thrown in a Coroutine

    and not handled directly within the Coroutine with a try-catch block, the Coroutine completes exceptionally. 2. Then, the exception is propagated up the job hierarchy until it reaches either the RootScope or a SupervisorJob. 3. When the root coroutine was started with launch{}, the exception will be passed to an installed CoroutineExceptionHandler. When the root coroutine was started with async{}, the exception is encapsulated in the Deferred.
  69. Recap 4. Keep in mind that when your Coroutine is

    not completing exceptionally, parent and sibling coroutines won’t be canceled. 5. Keep in mind that suspend functions can throw a CancellationException at any point and by catching them the Coroutine will keep on running.
  70. Why exception handling with Kotlin Coroutines is hard and how

    to master it! @LukasLechnerDev www.lukaslechner.com