Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Lukas Lechner Freelance Developer & Online Instructor www.lukaslechner.com @LukasLechnerDev

Slide 3

Slide 3 text

Kotlin Coroutines: Write asynchronous and concurrent code in a sequential fashion by using conventional coding constructs.

Slide 4

Slide 4 text

Exceptions in General

Slide 5

Slide 5 text

fun main() { // some code throw Exception() } >_ Exception in thread "main" java.lang.Exception

Slide 6

Slide 6 text

fun main() { // some code throw Exception() }

Slide 7

Slide 7 text

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") } }

Slide 8

Slide 8 text

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() }

Slide 9

Slide 9 text

Exceptions in Coroutines

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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) }

Slide 12

Slide 12 text

... 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() }

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

... 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") } }

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Coroutine Exception Handler

Slide 21

Slide 21 text

Every Coroutine and CoroutineScope has a CoroutineContext CoroutineContext Context Elements Job ExceptionHandler Name Dispatcher

Slide 22

Slide 22 text

val ceh = CoroutineExceptionHandler { coroutineContext, exception -> println(“Handled $exception in CoroutineExceptionHandler") }

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Root Coroutines have some special properties

Slide 32

Slide 32 text

Coroutine Exception Handler VS try/catch

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

There are 2 important aspects to consider when using try/catch !

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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)) }

Slide 48

Slide 48 text

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)) }

Slide 49

Slide 49 text

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)) }

Slide 50

Slide 50 text

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)) }

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

2. You might catch a CancellationException and therefor your coroutine continues to run

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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()

Slide 63

Slide 63 text

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 ...

Slide 64

Slide 64 text

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()

Slide 65

Slide 65 text

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()

Slide 66

Slide 66 text

ExceptionHandling launch{} VS async{}

Slide 67

Slide 67 text

launch{} rootScope

Slide 68

Slide 68 text

launch{} rootScope Coroutine Exception Handler

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

fun main() { val rootScope = CoroutineScope(Job()) rootScope.async { throw Exception() } Thread.sleep(100) } •>_ No Output

Slide 73

Slide 73 text

async{} rootScope

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

async{} Child Coroutines

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

... 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

Slide 92

Slide 92 text

Scoping Function coroutineScope{}

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

coroutineScope{} re-throws uncaught exceptions of its child coroutine

Slide 100

Slide 100 text

coroutineScope{} re-throws uncaught exceptions of its child Coroutines and so we can handle them with a try/catch

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

Scoping Function supervisorScope{}

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

Coroutine 2 Coroutine 3 Coroutine 4 Coroutine 1 SupervisorJob supervisorScope

Slide 115

Slide 115 text

Coroutine 2 Coroutine 3 Coroutine 4 Coroutine 1 SupervisorJob

Slide 116

Slide 116 text

Coroutine 2 Coroutine 3 Coroutine 4 Coroutine Exception Handler Coroutine Exception Handler Coroutine 1 SupervisorJob

Slide 117

Slide 117 text

Recap

Slide 118

Slide 118 text

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") } } }

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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 re-thrown when calling.await() 2 1 2

Slide 122

Slide 122 text

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") } }

Slide 123

Slide 123 text

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.

Slide 124

Slide 124 text

Further Resources www.lukaslechner.com/coroutines-exception-handling-cheat-sheet www.lukaslechner.com/why-exception-handling-with-kotlin-coroutines-is-so- hard-and-how-to-successfully-master-it/ github.com/LukasLechnerDev/Kotlin-Coroutine-Use-Cases-on-Android

Slide 125

Slide 125 text

Further Resources www.lukaslechner.com/coroutines-on-android

Slide 126

Slide 126 text

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