Tech Showcases,
Developer Resources &
Partners
droidcon News
Why Exception Handling with Kotlin Coroutines is hard and how to master it
By
Lukas Lechner
droidcon Americas 2020
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.
Transcript
English
00:10
all right we are live greetings from austria and welcome everybody to this talk and thank you for participating in this talk and i'm really excited to talk to you now about why exception handling in kotlin cool jeans is hard and how to master it my name is lucas lechner and i'm not only a freelance android developer but also an online instructor so this means that i produce a lot of educational content around android development and you can find a lot of my stuff on my website lucaslegna.com my latest pic project was actually a video course about cutting proteins but now let's start with the topic so let's talk about kotlin proteins in general the main goal of coroutines is that we are basically able to write asynchronous and concurrent code in a sequential fashion and by using only conventional coding constructs so kotlin coroutines try to be very intuitive so even for developers who have not much experience with coroutine it shouldn't be that hard to figure out what according based code is actually doing because well the code is written in a sequential fashion and it uses only conventional coding constructs and for the most part i think kotlin coordinates do a good job in being very intuitive however there is one part of core teams that is not that intuitive and you probably guessed it already this part is exception handling
00:15
so let's talk about exception handling and the goal of this talk is actually to explain exception handling with quarantines in a simple way so that exception handling in quarantines also gets intuitive for you but first let's talk a little bit about exceptions in general here you can see a simple jvm main function that has some code in it which throws an exception and when we run this main function obviously the app will crash and the exception will be locked out and this is because the exception isn't handled anywhere so it will be passed to the threads uncode exception handler and usually this means that the app is terminated
00:20
all of my examples will actually be jvm main functions for simplicity reasons but on android pretty much the same thing would happen lockheed would print the exception it would be passed to the threads uncalled exception handler and the app would crash so what can we do against this of course we can simply add a try catch and surround the code that throws an exception to the try block and then catch the exception the catch block and handle it there and when we now run the app again then we can handle the exception and no crash
00:25
if the exception is not thrown directly in the code block but in another regular function then this exception if it's not handled in the function itself will be rethrown so this means that we also can use a try catch on the call side so here as you can see in the main side main in the main function to handle the exception and if we run this app again we are able to handle the exception
00:30
so exception handling in general kotlin code is actually pretty simple if an exception is thrown at some point it will be re-thrown up the call stack until it's handled with a try catch block at some point but if it's never actually caught then it will be passed to the threads uncode exception handler and the app will be will crash now let's talk a little bit about exceptions in coroutines actually
00:35
here i again have a jvm main function and at first i define a root scope which is basically a coordinate scope and in this root scope we launch a new core routine and this core routine only throws an exception and if we execute this main function then of course the app will crash and this is actually what we expect because well we throw an exception but we handle it nowhere
00:40
what can we do in the core routine to handle the exception this is also simple again just use a try catch so when we run now this app again we can now again successfully handle the exception
00:45
if the exception is not thrown directly in the coroutine but in the suspend function then like in a regular function the exception will be rethrown so as you can see here again we are able to use a try catch on the call side and handle the exception so actually exception handling in quality based code also doesn't look that complicated right so why am i actually talking more than half an hour about this topic if it's also that simple well in the next example i show you when exception handling gets really tricky so let's put our exception back into the croutin itself and then in the try block let's just you use the launch block to start another cool key and let's let's take a look at this example and let's think about for some seconds what do you think will happen when we run this app so i take a pause for some seconds
00:50
and now let's run this example and as you can see unfortunately our app crashes and this is i think for a lot of developers pretty confusing because core teens promise us that we can use conventional coding struct constructs like the try catch here in order to write our asynchronous code and here we clearly throw an exception inside the try block but for some weird reason the exception isn't handled and this is well not that intuitive but what actually happens here well in order to understand what's going on we have to take a look look at the job hierarchy so as you probably know core routines have this new innovative concept of structured concurrent currency which basically makes asynchronous code much safer and in order for some of the mechanics of structure concur currency to work the the jobs of the core teams need to form a hierarchy and the top hierarchy for this example will look like this when we create a root scope we basically had a job at the very top and then when we launch a root coroutine we have another child job and when we launch launch another child protein then we have yet another child chop
00:55
and now when this child coroutine throws an exception and this exception isn't handled directly in the courtroom itself the launch quality will complete exceptionally so when the cool team completes exceptionally then the exception is not rethrown like a regular function otherwise we would be able to handle it without try catch here but instead the exception will travel up the top hierarchy so it first will go to the parent shop the root care routine and it will cancel the root coroutine but it would also go way up to the root scope and since it's not handled nowhere when it travels up the top hierarchy it's passed to the threads unc to the thread's uncode exception handler and that's actually the reason why this code example crashes
01:00
but how can we actually avoid crashes like this so how can we avoid exceptions that are not caught by a track catch but instead travel because but instead trail up the job hierarchy well we can handle them by using a coordinating exception handler but how can we actually install a coordinate exception handler in our code so as you might know every core routine and also every quarton scope has a coordinating context and in the coroutine context we can add some additional context elements to our to a quartering or to according scope and these elements add some additional technical information and some context elements are for instance the dispatcher the job of the coroutine the name of the coroutine but also the exception handler so we can basically install an exception handler by passing it as a context element to a cool routine or a quoting scope
01:05
so how can we create a coordinate exception handler here you can see the code we simply call the quartenexceptionheader function and the two input parameter input parameters are the coding context which is the context of the coroutine in which the exception handle is called and also the exception and in the in the body of the exception handler will simply print out that we handle the ex the exception and this is actually the encoding exception handler that i will use for the following examples so let's go back to our previous example and as you can see on the top i created the coordinate exception handler and now we can install it at some point in our top hierarchy so i said we could install it in the scope or in a root or in a root core routine or in a child routine but since the child coroutine actually throws the exception we could simply install it there by adding it to the launch quality builder because for every coating builder we can pass context elements
01:10
unfortunately when we run this app again it still crashes and this is also a little bit confusing because what happens here is that when the child protein throws the exception and this exception is not handled directly in the coroutine the corten will again fail and the exception will again be propagated up the chop hierarchy and what this means is that also exception handling will be delegated up the top hierarchy and since the root care routine doesn't have a quality exception header installed again the threads uncode exception handler will be called and this leads to the crash of this example so in order to fix this we have to install it in the root croutin so now when the exception is shown again it is propagated up the top hierarchy and then handled by the coordinate exception handler and finally our app crashes don't doesn't crash anymore but instead we're able to handle the exception the last possibility would be to install it in the root scope and since when we launch new quarantines in this scope we the coroutine inherits the context elements of the scope the root quartin inherits also the coordinate exception handler so also in this case we are able to handle the exception
01:15
so as you can see root coordinates have some very special properties and the first special properties is just that we can only install coordinating exception handlers in route coroutines and not child coordinates in other places root coroutines are also called top level quartins but the official documentation calls them root corotines so i will also stick with that term
01:20
all right now you basically know how to handle exceptions in quotient based code we can either install a code in exception handler in the job hierarchy or we could use try catch directly in the core team but the big question now is when should we use which of these two options
01:25
well when you read the documentation of the quotient exception handler you will read that it is it should be used as a last resort mechanism for global catch-all behavior so if you install a courtney exception handler in a root core routine it basically will catch all exceptions of failing child proteins the problem here is that the quotients have already completed and therefore we are not able to recover from the exception some use cases for coordinate exception handlers are to lock the exception to a crash reporting tool or to show an error message to the user
01:30
what you shouldn't do is just install an empty query exception handler in all your root co core routines just to avoid crashes because this would be like an empty catch block and when you do this then a lot of problems can stay unnoticed for a long time so please don't do this on the other hand with try catch we can handle the exception directly in the quality itself and therefore we have the possibility to recover from this exception so some use cases for try catch in a cool team would be to retry the operation so for instance when a network request fails we could simply retry it but of course we could also perform other average arbitrary operations on exceptions so for instance when the network request fails we could load the data from the database instead however you need to be very careful when you use try catch in your coatings because there are two very important aspects you need to consider when you use tri-catch and the first one is that we need to keep the cancellation mechanism of structured concurrency in mind so what do i mean by that let's have another look at an example here we again use a root scope to launch another root coroutine and in this root coroutine we launch two child coroutines with async and each of this child coroutine performs a network request then we also store the results of the network request in the result value and then we display the result to the user and what's important to mention here is that we only want to show data to the user in case when both network requests are actually successful so we don't want to show like partial data if only one of those two is successful so when we have a look at this job hierarchy then you can see on the top again the root scope then they launch root coroutine and then two async child proteins and this code example actually works as expected
01:35
since when one network request actually throws an exception then the async charcoal routine will complete exceptionally or in other words words it will fail this exception will be propagated up to the launch root coroutine which then will be we will be cancelled too but also the other async child protein will be cancelled immediately and that means that the user immediately sees an error message and we're not wasting any device resources after the first network request fails so the code as we have here works as expected however then our project manager comes and says to uh to our developers that well it seems that network request fails quite often so can we analyze that a bit please could we maybe track the exceptions to our crash reporting tool and of course we as developers we implement this requirement and the first thing we might think of is simply use try catch here and so every time the network request now fails the exception enters the catch block in the catch block we can then lock the exception to our crash reporting tool and our project manager is happy right well not that much because by doing that we actually introduced a problem because now when network request one fails the exception will be will be handled in the catch block so this means that the async child protein won't fail and this means that it will complete exceptionally so when the first network request fails the second quarten will continue to run and this wastes a lot of device resources and the user will wait longer for the error message so by adding a try catch here we introduce the problem the right solution for this requirement would be to instead use create a quality exception handler and install it in the root scope
01:40
so let's move on the second very important aspect when using track catch is that you might catch a cancellation exception and therefore your core team continues to run so i want to explain this issue here by with another code example in which we first launch a new coroutine and store the job of the code in its own variable and in the core routine we first print out that we are starting that network request then we delay for 1000 milliseconds in order to simulate the network request and then we print out that the protein is still running
01:45
below the coroutine we first delay for 500 milliseconds and then we cancel the codeine so we are basically cancelling the cool thing while the network request is still in flight and this code as it is here works as expected as desired and is correct because only starting network request will be printed out and after 500 milliseconds the delay suspend function will actually throw a cancellation exception and therefore the quality will complete exceptionally and stops its execution so cortine still running will not be printed out
01:50
now again we have this requirement to investigate the exceptions of this network request so again we might think of just using a try catch here
01:55
but again we introduced a problem here because when we run this this example then at first starting network request will be printed out and after 500 milliseconds delay will again throw a cancellation exception but since we catch it in our catch block the quality will not complete exceptionally but will continue to run so also coroutine still running will be printed out and this is a problem and again wastes a lot of device resources because we cancel the coroutine but it keeps on running and in some cases this could even lead to crashes
02:00
the fix for this would be to only catch hdb exceptions or rethrow the cancellation exceptions
02:05
okay let's move on to the exception handling differences between proteins that are started with launch and proteins that are started with async so up until now we only used actually launch to start root croutines and as a reminder if a launch coating fails then the exception travels up to the root scope and if a quality exception handler is installed the exception will be passed to this handler but now let's take a look at what happens when we actually use async to create a root core key and as you can see in this root asynchronous coroutine we simply throw an exception and when we now run this app then as you can see actually there is no output so what happens with this exception well when we use an async root coroutine then when this coordinate fails the exception will also be propagated up to the root scope and other siblings of this child protein will be would be cancelled however the exception itself won't be passed to the coating exception handler but instead it will be passed it will be encapsulated in the deferred object of the async team what is the deferred object well the launch coroutine builder returns a job the async coding builder returns a deferred which is basically a job a result type so it's a special kind of chop and this deferred also encapsulates all exceptions that happen and this exception will only be rethrown when we call a weight on them so let's call a weight on it but first store store the deferred in its own variable and then call await on it but since a weight is a spend function we have to use launch to start another core routine and then we can use try catch to to handle this exception but we could also use a cooling exception header to handle it
02:10
it becomes a bit more tricky when async is a child coroutine so in this example now we use launch to start a root coroutine and async to start a new child protein and in this child routine we threw an exception and before when async was a root coroutine we actually had no output but when we run this app then actually our app crashes and this is because well as i said before also with async core routines the exception travel up to the root scope and then it really depends on the root coordinate if the root coroutine is started will start with launch then the exception will be passed to the coordinate exception handler and since no exception handler is stalled in this case we have the crash
02:15
if this route could routine would be started with async then again the exception that travels up from the child protein would be encapsulated in the deferral object
02:20
okay for the last part of my talk today i want to talk a little bit talk a little bit about the scoping functions cortine scope and supervisor scope and how they behave when it comes to exception handling let's first talk about the cultin scope scoping function here you can see the example that i had at the very beginning of my talk in which we had a try catch in a coroutine and in the try block we launched another core team and as you saw we this example crashed however something very interesting happens when we now use the cooling scope scoping function and surround the launch block with it because now we are able to handle the exception and this is because corotin scope actually rethrows the uncut exceptions of its child cool kids and therefore we are able to handle them with a try catch
02:25
the main use case for the quality scope scoping function is to use it in suspend function so that we get access to the protein scope that calls this suspend function and so we are able to start new coroutines in the suspect function itself and every time you call such a suspend function you can be sure that try catch catches all the exceptions that are thrown in courtings that are started in the suspend function
02:30
let's now talk about the other scoping function which is the supervisorscopescoping function and what supervisorscope really does is it creates a new independent child scope scope in our job hierarchy and independent means when we talk about exception handling so let's talk about this example here in which we have a root scope in which we launch a new root coroutine and then we also launch a child protein and then we open up a supervisor scope and as you can see in the top hierarchy on the right by doing this we are basically installing a supervisor job in our job hierarchy and in the supervisor scope we launched two additional proteins and
02:35
these qualities are now proteins in the supervisoscope but what does it mean that these are proteins in the supervisor scope well as i said before the supervisors group scope creates a new independent scope when it comes to exception handling so when one of these two coating fails then we have to handle the exception directly in the supervisor scope and the exception won't propagate up any further so when one of the quarantine fails in order to handle the exception we have to install a curtin exception handler in one of those cool kits
02:40
but if you can remember when we talked about the quilting exception handler i said that we can only install it in root croutins but here it seems that these proteins aren't really root cortines because they are really on the bottom of the job hierarchy well the interesting thing is that coroutines that are started directly in the supervisor scope are now top level quartens and therefore we can actually install a coordinate exception handler in there and that's crucial to understand all right this was the main part of my presentation and now i simply want to reca recap the most important points of this talk and the first important point is that the exceptions can be handled in quarantines in the conventional way simply by using try catch and when we do this then the cool thing doesn't complete exceptionally however when we don't handle the exception directly in the coroutine the quality will complete exceptionally and the exception will in this case travel up the top hierarchy and when it travels up until it will travel up until it either hits hits a root scope or a supervisor chop and while it travels up the parent and other sibling proteins will be cancelled
02:45
another important point is that exceptions are treated differently when the root protein was either started with launch and async with launch route quarantines the exception will be passed to the query in exception handler but with async root quartens it will be encapsulated in the deferred object and only rethrown when we call a weight on it
02:50
important point number five is that by using the scoping function coordinate scope we are all exceptions of child proteins of that quarantine scope will be rethrown and therefore we are able to handle the exception with a try catch block and the last important point is that you need to keep in mind two things when you use try catch in your core kids when your core keen is not completing exceptionally then the exception won't travel up the stop shop hierarchy and so parent cortens and simply carotenes won't be cancelled and the other important aspect is that suspend functions can throw a cancellation exception at any point and if we catch them then the coating will keep on running so we have to rethrow the cancellation exception in order to stop the execution if you're further interested in exception handling with kotlin quarantines i created a kotlin courtin's exception handling cheat sheet which basically contains the six important points that i mentioned before and i also created an extensive article about exception handling with kotlin core routines which you can find on my website and i also have a very popular github project kotlin quotient use cases on android in which i have the most common use cases for using core routines on android and each use case of course comes with appropriate exception handling another good resource is my video course about mastering kotlin co routines for android development which you can also find on my website and this course also has a big section about exception handling in cool kids
02:55
and that's about it i thank you very much again for participation i hope you now understand exception handling with core teams and you are able to use them effectively in your own application thank you very much
03:00
all right we have some time for questions
03:05
okay what tool is being used to show the code on slides and do that fancy animation between code changes so this is a keynote so the powerpoint of mac basically and with that it's very powerful and you can do things like this like that next question is
03:10
does flow api make it easier to deal with exceptions um i'm not really an expert in flow yet i haven't taken a look into it but from from a first i i think that it will be easier yes it will be similar to rx java so i think it's a bit easier easier but i can't say because i'm not an expert in flow really
03:15
why did the try catching the root coroutine could not handle the exception of the gi protein well because when the charcoal routine when the exception is thrown in the chalk protein and not handled directly in the quarten then the exception isn't rethrown like for instance when with a regular function but instead it travels up the job hierarchy so that's actually the most confusing thing so travels up the top hierarchy and therefore the try catch is not effective anymore
03:20
can you please elaborate on what's root coordinate scope and what is a child coordinate scope thanks so i called it root scope actually my examples because you as you can so in the end of my presentation you can basically install subscope subscopes in every shop hierarchy and yet such as subscope that for instance can be opened with the coating scope or the supervisor scope scoping function would then be a child coordinate scope
03:25
sorry in loop question what is according scope how are they related to life cycle so a coltin scope pretty much defines how long your core teams are running so it defines the the lifetime of the proteins and it basically makes sure that when the cooling scope the lifetime of the coating scope ends then all of the coatings are cancelled and so that we avoid stuff like memory leaks next question is should we ever declare our job to be a supervisor job in the coating scope that we create
03:30
um yeah i mean if you're familiar with the view model scope of the android x libraries then this view model scope actually uses a supervisor shop and this is necessary because in some cases we don't want to cancel sibling coroutines whenever one quartering actually fails and by installing a supervisor job we avoid this so basically in the android view model we don't want that for instance when one coating fails that the whole ui isn't working anymore because you can't start recordings anymore so yeah it makes sense in some cases
03:35
great talks this was fun yeah i also think so um when should you use try catch instead of according exception handler well i explained this in my my talk maybe i will share this lights afterwards on the platform here i had a table with some information about when to use each one and when what are some typical use cases for each option
03:40
next question is coding scope similar to a separate thread in java no not really
03:45
not really
03:50
is there a nice way to handle cancellation exception more globally unfortunately the problem is the cancellation exception won't be won't won't be passed into into a cooling exception handler and therefore i'm not sure how what's the best way to to handle this globally
03:55
can you share the presentation deck yeah i will post a link to it on the platform on my on the proof on the page of this talk
04:00
when using a coding scope can it throw a cancellation exception well every suspend function can basically throw a cancellation exception and before you can basically call spent functions you need to start a codeine so yes is the answer so yeah in in the cool thing that was started in the coding scope there could be a cancellation exception be thrown
04:05
what enables this cooperative cancellation between proteins this is because yeah well because of structure concurrency and because there's this there's this hierarchy that's where the choppers are connected with each other and this basically enables that when one fails and the other one will be cancelled
04:10
all right i think no more questions and the time is also over so thanks again for your participation and i will post the links on the platform and yeah have a nice evening or nice whatever day bye
droidcon News
Tech Showcases, Developer Resources & Partners
EmployerBrandingHeader
jobs.droidcon.com
![]() Latest Android Jobs
Kotlin Weekly
![]() Your weekly dose of Kotlin
ProAndroidDev
![]() Android Tech Blogs, Case Studies and Step-by-Step Coding
Zalando
![]() Meet one of Berlin's top employers
Academy for App Success
![]() Google Play resources tailored for the global droidcon community |
Droidcon is a registered trademark of Mobile Seasons GmbH Copyright © 2020. All rights reserved.