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

Why Exception Handling with Kotlin Coroutines is hard and how to master it

Why Exception Handling with Kotlin Coroutines is hard and how to 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

November 17, 2020
Tweet

More Decks by Lukas Lechner

Other Decks in Programming

Transcript

  1. Why Exception Handling in Kotlin Coroutines is hard … …

    and how to master it! Lukas Lechner Freelance Developer & Online Instructor www.lukaslechner.com November 17, 2020
  2. Kotlin Coroutines: Write asynchronous and concurrent code in a sequential

    fashion by using conventional coding constructs.
  3. fun main() { // some code throw Exception() } >_

    Exception in thread "main" java.lang.Exception
  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") } }
  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() }
  6. fun main() { val rootScope = CoroutineScope(Job()) rootScope.launch { throw

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

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

    { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } Job Coroutine started with launch{} rootScope Job
  14. 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. 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
  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. val ceh = CoroutineExceptionHandler {…} val rootScope = CoroutineScope(Job()) rootScope.launch

    { try { launch { throw Exception() } } catch (exception: Exception) { println(“Handled $exception") } } Child Coroutine Root Coroutine rootScope
  18. 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
  19. 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
  20. 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
  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
  23. 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
  24. 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
  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)) }
  31. rootScope.launch { val result1 = async { networkRequest1() } val

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

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

    result2 = async { networkRequest2() } display(awaitAll(result1, result2)) } async launch rootScope async
  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

    { networkRequest1() } 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. 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)) }
  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 async async rootScope.launch { val result1 = async

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

    result2 = async { networkRequest2() } display(awaitAll(result1, result2)) }
  41. val ceh = CoroutineExceptionHandler {…} rootScope.launch { 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 ceh = CoroutineExceptionHandler {…} rootScope.launch(ceh) { val result1 =

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

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

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

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

    delay(1000) } catch (e: HttpException) { // Log Exception println("Caught: $e") } println("Coroutine still running ... ") } delay(500) job.cancel()
  48. 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()
  49. fun main() { val rootScope = CoroutineScope(Job()) rootScope.async { throw

    Exception() } Thread.sleep(100) } •>_ No Output
  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() } rootScope.launch

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

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

    ceh = CoroutineExceptionHandler { ... } rootScope.launch(ceh) { deferredResult.await() }
  54. ... 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
  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. ... rootScope.launch { try { launch { throw Exception() }

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

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

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

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

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

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

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

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

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

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

    } supervisorScope { launch { // Coroutine 3 } launch { // Coroutine 4 } } } Coroutine 2 SupervisorJob Coroutine 1 rootScope
  68. 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
  69. 1) Exceptions can be handled directly in a Coroutine with

    a try-catch block. This way, the Coroutine doesn’t complete exceptionally. launch { try { throw Exception() } catch (exception: Exception) { println("Handled $exception") } } }
  70. 2) If an exception is thrown in a Coroutine and

    not handled directly within the Coroutine with a try-catch block, the Coroutine completes exceptionally. launch { throw Exception() } Coroutine
  71. 3) As a result, the exception is propagated up the

    job hierarchy until it reaches either the RootScope or a SupervisorJob. While the exceptions travels upwards, parent coroutines fail too and sibling coroutines get cancelled. Coroutine 2 Coroutine 4 SupervisorJob Coroutine 5 rootScope Coroutine 1 Coroutine 3 1 3 2 1
  72. 4) 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 object. async{} rootScope launch{} 1 Coroutine Exception Handler Deferred<T> re-thrown when calling.await() 2 1 2
  73. 5) The scoping function coroutineScope{} re-throws uncaught exceptions of its

    child Coroutines and so we can handle them with a try/catch. launch { try { coroutineScope { launch { throw Exception() } } } catch (exception: Exception) { println(“Handled $exception") } }
  74. 6) Keep in mind: • When your Coroutine is not

    completing exceptionally, parent and sibling coroutines won’t be canceled. • Suspend functions can throw a CancellationException at any point and by catching them, the Coroutine will keep on running.
  75. Why Exception Handling in Kotlin Coroutines is hard … …

    and how to master it! Lukas Lechner Freelance Developer & Online Instructor www.lukaslechner.com November 17, 2020