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.

B471eb2445c405a8d33f5777816550e3?s=128

Lukas Lechner

October 09, 2020
Tweet

Transcript

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

    to master it! @LukasLechnerDev www.lukaslechner.com
  2. Exceptions in General

  3. fun main() { // some code throw Exception() } >_

    Exception in thread "main" java.lang.Exception @LukasLechnerDev
  4. 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
  5. 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
  6. Exceptions in Coroutines

  7. fun main() { val rootScope = CoroutineScope(Job()) rootScope.launch { throw

    Exception() } Thread.sleep(100) } >_ Exception in thread "DefaultDispatcher-worker-1" java.lang.Exception
  8. 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) }
  9. ... 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() }
  10. ... rootScope.launch { try { throw Exception() } catch (exception:

    Exception) { println("Handled $exception") } } }
  11. ... 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") } }
  12. ... val rootScope = CoroutineScope(Job()) rootScope.launch { try { launch

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

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

    { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } Job Coroutine started with launch{} rootScope Job
  15. 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
  16. 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
  17. Coroutine ExceptionHandler

  18. Every Coroutine and CoroutineScope has a CoroutineContext CoroutineContext Context Elements

    Job ExceptionHandler Name Dispatcher
  19. val ceh = CoroutineExceptionHandler { coroutineContext, exception -> println(“Handled $exception

    in CoroutineExceptionHandler") }
  20. val ceh = CoroutineExceptionHandler {…} val rootScope = CoroutineScope(Job()) rootScope.launch

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

    { try { launch { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } Child Coroutine Root Coroutine rootScope
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. Root Coroutines have some special properties

  29. CoroutineExceptionHandler VS try/catch

  30. 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
  31. There are 2 important aspects to consider when using try/catch

  32. 1. Keep the “Cancellation Mechanism” of Structured Concurrency in mind

  33. rootScope.launch { val result1 = async { networkRequest1() } val

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

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

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

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

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

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

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

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

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

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

    { networkRequest1() } val result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  44. 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)) }
  45. 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)) }
  46. 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)) }
  47. 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)) }
  48. rootScope.launch { val result1 = async { networkRequest1() } val

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

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

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

    async { networkRequest1() } val result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  52. 2. You might catch a CancellationException and therefor your coroutine

    continues to run
  53. val job = launch { println("Starting network request") delay(1000) println("Coroutine

    still running ... ") } delay(500) job.cancel()
  54. val job = launch { println("Starting network request") delay(1000) println("Coroutine

    still running ... ") } delay(500) job.cancel()
  55. val job = launch { println("Starting network request") delay(1000) println("Coroutine

    still running ... ") } delay(500) job.cancel()
  56. val job = launch { println("Starting network request") delay(1000) println("Coroutine

    still running ... ") } delay(500) job.cancel()
  57. val job = launch { println("Starting network request") delay(1000) println("Coroutine

    still running ... ") } delay(500) job.cancel()
  58. val job = launch { println("Starting network request") delay(1000) println("Coroutine

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

    delay(1000) } catch (e: Exception) { println("Caught: $e") } println("Coroutine still running ... ") } delay(500) job.cancel()
  60. 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 ...
  61. val job = launch { try { println("Starting network request")

    delay(1000) } catch (e: HttpException) { println("Caught: $e") } println("Coroutine still running ... ") } delay(500) job.cancel()
  62. 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()
  63. ExceptionHandling launch{} VS async{}

  64. launch{} rootScope

  65. launch{} rootScope Coroutine Exception Handler

  66. fun main() { val rootScope = CoroutineScope(Job()) rootScope.async { throw

    Exception() } Thread.sleep(100) }
  67. fun main() { val rootScope = CoroutineScope(Job()) rootScope.async { throw

    Exception() } Thread.sleep(100) }
  68. fun main() { val rootScope = CoroutineScope(Job()) rootScope.async { throw

    Exception() } Thread.sleep(100) }
  69. fun main() { val rootScope = CoroutineScope(Job()) rootScope.async { throw

    Exception() } Thread.sleep(100) } •>_ No Output
  70. async{} rootScope

  71. async{} rootScope Coroutine Exception Handler Deferred<T> re-thrown when calling .await()

  72. fun main() { val rootScope = CoroutineScope(Job()) rootScope.async { throw

    Exception() } Thread.sleep(100) }
  73. ... val deferredResult = rootScope.async { throw Exception() }

  74. ... val deferredResult = rootScope.async { throw Exception() }

  75. ... val deferredResult = rootScope.async { throw Exception() } deferredResult.await()

  76. ... val deferredResult = rootScope.async { throw Exception() } deferredResult.await()

  77. ... val deferredResult = rootScope.async { throw Exception() } rootScope.launch

    { deferredResult.await() }
  78. ... val deferredResult = rootScope.async { throw Exception() } rootScope.launch

    { deferredResult.await() }
  79. ... val deferredResult = rootScope.async { throw Exception() } rootScope.launch

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

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

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

    ceh = CoroutineExceptionHandler { ... } rootScope.launch(ceh) { deferredResult.await() }
  83. async{} Child Coroutines

  84. ... val rootScope = CoroutineScope(Job()) rootScope.launch { async { throw

    Exception() } }
  85. ... val rootScope = CoroutineScope(Job()) rootScope.launch { async { throw

    Exception() } }
  86. ... val rootScope = CoroutineScope(Job()) rootScope.launch { async { throw

    Exception() } }
  87. ... val rootScope = CoroutineScope(Job()) rootScope.launch { async { throw

    Exception() } }
  88. ... 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
  89. Scoping Function coroutineScope{}

  90. ... rootScope.launch { try { launch { throw Exception() }

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

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

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

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

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

    } } } catch (exception: Exception) { println(“Handled $exception") } } >_ Handled exception
  96. coroutineScope{} re-throws uncaught exceptions of its child coroutine

  97. coroutineScope{} re-throws uncaught exceptions of its child coroutine and so

    we can handle them with a try/catch
  98. suspend fun suspendFunction() = coroutineScope { // suspend function body

    }
  99. suspendFunction() suspend fun suspendFunction() = coroutineScope { // suspend function

    body }
  100. suspendFunction() suspend fun suspendFunction() = coroutineScope { // suspend function

    body }
  101. try { suspendFunction() } catch (exception: Exception) { println(“Handled $exception")

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

    } suspend fun suspendFunction() = coroutineScope { // suspend function body }
  103. Scoping Function supervisorScope{}

  104. supervisorScope{} a new independent child scope in our job hierarchy

  105. rootScope.launch { // Coroutine 1 launch { // Coroutine 2

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

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

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

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

    } supervisorScope { launch { // Coroutine 3 } launch { // Coroutine 4 } } } Coroutine 2 SupervisorJob Coroutine 1 rootScope
  110. 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
  111. Coroutine 2 Coroutine 3 Coroutine 4 Coroutine 1 SupervisorJob supervisorScope

  112. Coroutine 2 Coroutine 3 Coroutine 4 Coroutine 1 SupervisorJob

  113. Coroutine 2 Coroutine 3 Coroutine 4 Coroutine Exception Handler Coroutine

    Exception Handler Coroutine 1 SupervisorJob
  114. 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.
  115. 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.
  116. Further Resources https://kotlinlang.org/docs/reference/coroutines/exception-handling.html https://www.lukaslechner.com/why-exception-handling-with-kotlin-coroutines- is-so-hard-and-how-to-successfully-master-it/ https://www.lukaslechner.com/coroutines-on-android https://github.com/LukasLechnerDev/Kotlin-Coroutine-Use-Cases-on-Android

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

    to master it! @LukasLechnerDev www.lukaslechner.com