In-Depth Guide to Coroutine Cancellation & Exception Handling - Android Studio Tutorial

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey guys and welcome back to a new video in this video we will actually deep dive into exception handling and cancellation mechanisms for core routines on the surface core routines are super simple and especially if you just learn them and it feels like just async and a weight in javascript or so then it seems super easy a super great tool to actually use for asynchronous programming and they are a really great tool to do that but if you dive deeper into core routines there are actually a lot of traps you can step into there are a lot of complications and a lot of things that are not trivial like exception handling and cancellation mechanisms which i will cover here so it's not like just using a trying catch block you can do a lot of things wrong if you do that so i want to explain how you should catch your exception of how you should handle them in curotenes how exception handling actually in general works for coroutines and what you actually need to consider when your curtains are canceled or when you cancel your own coroutines so first of all how most people think exception handling works for curtins let's say we have a curtain here in lifecycle scope that we launch and then we have a childcare routine here in which we could throw an exception so far so good many people now think they can just use a try and catch block to catch that exception so e exception and we could say we print chord exception e if we now launch this and take a look in logcat clear this then you can see that exception was actually not caught so it made our app crash even though we actually used a try and catch block so that's how exception handling and proteins does not work so how does it work well to understand that we first of all need to understand how curtin scopes and curtains in general work so at the outside we usually have such a curtain scope like life cycle scope view model scope or just your own custom scope and we launch a carotene inside of that scope and then inside of that curtain scope we have the option to launch more of these so-called childcare routines which would be this one here and these two just run independently of each other and now what happens if we throw an exception in here so that could for example come from an api call let's say use retrofit and yeah there's for example an http exception so your server replies with a 404 not found or so and that will throw an exception from from retrofit and since that is usually executed in a curtain or you can at least do that that would be a normal typical thing where such an exception could occur and now what happens with this exception if it is not handled directly by using a try and catch block and i don't mean such a trying catch block i mean one directly in the curtain where it's thrown so in here that would however work if that is not the case so if it's uncaught basically what will happen is it will simply be propagated up these coroutines so let's do it a little bit more um like let's launch another one to make this even clearer if we throw an exception in this child protein of this child carotene this exception would be propagated up to this curtain first it will see okay there's an exception let's propagate it up to the next one which would be this one it's still not handled here so we will propagate it up to the next one and here it actually reaches the outer core routine and it's not handled so at this point our program will crash and that's exactly the same what also happens with cancellations because when a curtain is cancelled it will throw a cancellation exception a cancellation exception is not a bad one in the sense that it will make your program crash it actually won't because these are always handled or actually caught in coroutines but these are still propagated up your curtin tree kind of so that all core routines like the parent and the child ones and the child ones of the child ones actually all know that a specific curtin was cancelled however now we only covered the case that we use launch but if you know curtins a little bit or maybe watch my basics playlist about that then you know that there's also a different way to launch a coroutine which is called async async will return a so-called deferred like in comparison to launch which will just return a job async will return a deferred what's the difference well launch is just used to do something we don't expect launch to give us any result however with async we expect that result so let's say you do a network call and you are interested in that result but you still want to execute execute that that will call independently of another croutin then you could use async and actually get that result using a weight so for example we could remove this and we could say val um i don't know string is equal to async and here we could delay this block for like 500 milliseconds and then return our string um that will be returned here in our async block and that will be executed however if we then want the result of that async block as you can see if we um see the string is not deferred of type string so it's not a string yet if we want the result we can say for example print line string dot await so that's just important to understand because there are differences how exceptions are handled when it comes to launch versus async so here if we print this line this await block would simply suspend the square routine like this launch block here until that result is actually available which would be after half a second and the difference now how exception handling works for async in comparison to launch is that async will actually kind of throw the exception that it has accumulated when we call a wait so let's say we would do a network call here kind of simulate that and that network call would throw an exception something like this then this wouldn't immediately make our program crash it would only make it crash once we call a wait while if we use launch it will make our program crash as soon as we throw that exception so if we now remove this await relaunch this you will notice something that our program still crashes so why is that even though we used async and i said that it will only throw or raise that exception when we actually call a wait on that defer that it returns however if we think about the mechanism how exceptions are propagated again what will happen here is it will throw that exception but since it's a child care routine it will propagate that up to the parent coroutine which is this one and what we do what do we see here it's a launch block so again since we have a launch block here it will also handle that exception like any launch box so it will immediately make our program crash if it's not handled if however we also swap this out with async and we relaunch our app take a look at logcat then you can see nothing will happen because we use two async blocks here this exception is propagated up to this curtain and to this curtain but since both these are async blocks they won't immediately make our program crash they will actually leave that up to the user and with the user i mean whoever actually yeah calls await on these two asyncs or actually not on these two um this outer one should be enough so if we now have a caller of that let's say another lifecycle scope launch block so imagine this will just be a deferred that comes from a library and that returns a result after doing some async stuff and you now want to consume that deferred in a launch block and you say okay you have that deferred and you actually consume that here using deferred.weight and we relaunch this then it will crash again because now we actually called wait here it will kind of yeah raise that exception since we can have the launch block it will make our program crash since we don't handle this here with try and catch so if you have a deferred then you can handle your exceptions just by surrounding this with try and catch here so if we catch this here the exception for example print this then we relaunch this and it won't crash it will just print the exception here as a warning lock but not as an actual error lock and not as a crash however if you catch your exceptions like that you are doing something wrong and not many people know about that i will get into this later once we learned a little bit more about how coroutines work so at the end of this video i will talk about this mistake here but definitely don't catch your exceptions in corinthians like that even though it works there are things that can go wrong but let's first of all think of a different way how we could also handle uncalled exceptions and like try and catch is of course one way of doing that and that must be in the curtain which um throws that exception or if you have an async one as i said you need to surround your weight call with that however what we can also do is we can use a so called coroutine exception handler so we can specify a specific handler which is a curtin exception handler here which gives us the curtin context so that just gives us information about the specific curo team that through that exception we don't need that here and we of course get the throwable we could print this here card exception and yeah print the throwable and if we now take this handler we can install this into a curtain that we launch and it must be at the root care routine where we install this so the root cause would be this async block or this launch block here um let me actually get rid of this async vlog and just get back to launch blocks here we just have a lifecycle and here in parentheses we could pass this handler and if we now throw an exception here and say error and we launch our app then we will see that it's suddenly not crashing any more but it prints we actually caught this exception so that is a way how we can actually catch exceptions in curotins that are propagated up so even if this now comes from a child curtin which we would launch in here where we throw this this would get propagated up to this outer curtain eventually and if we then take a look we still catch that exception so it will basically just um handle all uncaught exceptions that come from any type of child creatine in root care routine and it's also important to mention that this won't be called for cancellation exceptions so if one of your current teens here is cancelled this block won't be fired because as i said it will only be handled for uncovered exceptions however cancellation exceptions and curotenes are handled by default because you don't want your app to crash because the curtain was cancelled and as a last thing before we get to the common trap and mistake that i mentioned here i want to talk about two different scopes of coroutines that you can use in your apps that decide about how specific coroutine and child proteins are cancelled on the one hand we have the normal carotene scope and on the other hand we have the so-called supervisor scope so i'm sure if you know the curtin basics you will have seen things like this where you launch a curtin scope for example in the main dispatcher and you call that launch on that and a very common question i get is uh what's the difference between this and launching a curotene in global scope well with this curtin scope builder you can kind of create your very own carotene scope on the one hand but there are different differences between different types of protein scopes which determine how cancellation is handled in such a kurti let's try to understand how this works with such a normal curtain scope if we have child carotenes and let's say we delayed this for 300 milliseconds we saved this in a job so we could cancel it for example or let's actually don't need the job here but let's say we delay this for 30 milliseconds and then we throw an exception so then something goes wrong whatever we're currently doing and we say uh curtin one failed and then we have another independent curriculum of um yeah curtin that is independent of this one which would delay for 400 milliseconds so which just takes a little bit longer than the the other one but that one succeeds so when that finishes we say curtin 2 finished if we now launch this then this will probably be obvious what happens here you can see that for some reason it actually crashed on my phone uh let's relaunch this yeah now we can actually see the stack trace here you can see cartoon one failed and of course um if our app crashes it won't execute our other one that is pretty clear how about what happens if we now install a curtain exception handler what you just did and we say we caught an exception again like this and we install this handler into our curtain scope if we want to actually combine two curtain contacts such as this one with the handler we can simply use the plus operator to make sure we apply this here if we now relaunch this we will see we caught an exception our program didn't crash our app didn't crash however we don't see that curtin two actually finished so what happened here because i said these two corotine scopes are actually actually these two curtains are running independently of each other so why does this one seem to affect this one and the reason behind that is that we used curtin scope what kerosene scope will do is as soon as one carotene fails no matter if you handle that exception or not it will cancel all its child proteins and then also the whole scope so the whole scope will be cancelled as soon as just one curtain fails and with failing i mean that it uh throws an exception so here we thought that exception is propagated up to this curtain scope it will see okay a curtain failed so it will cancel the whole scope and uh yeah also all the other curtains running in that launch block and that is why this print line statement is actually never printed here in this case even though we actually catch that exception and that is now where we come to the other version of the curtin scope which i mentioned which is the supervisor scope we can directly apply that here instead of that launch block call that supervisor scope and put our two launch blocks in there and if we now relaunch this same thing as before just that we now have that supervisor scope you can see we caught that exception now but we also get the log that our curtain 2 finished so the difference now is that with supervisor scope if one coroutine in that scope here fails throws an exception that won't have any effect on the other proteins like this one in that scope so you can kind of group coroutines together and give them that behavior that they don't affect each other when they fail in the same way you could also group curtins together to have that behavior that if one fails then all should fail by simply using this curtin scope block here and that's actually a very important difference because sometimes in your app in your apps you need custom curating scopes because sometimes some components need their own or have their own custom lifetime and you can define that custom lifetime by using a curritin scope so that all the coroutines is used in that component will be properly cancelled when it's not used anymore for example such a scope would be the view model scope when the view model is cleared then all the curtains running in that viewmodel will also be cleared and to create such a custom scope many people just use this curtain scope here so this builder use some kind of dispatcher and maybe not even an exception handler and the big mistake and issue here is that if you do this for your own custom scope then if one coroutine in your whole component fails all others will fail your current scope will be cancelled and once the scope is cancelled you can't relaunch new coroutines in that so imagine that would actually be the case for viewmodelscope that would mean that as soon as one network call for example fails in a scope and throws an exception then yeah all the other currents in your view model will be cancelled the whole view model scope will be also cancelled and you can't can't launch new curritins unless you recreate your whole viewmodel that's of course not what we want and if we actually go to view model and we have the view model scope here where we can take a look into holding control and clicking on that we will see that behind this view model scope there is actually such a supervisor job and that is combined with the main immediate dispatcher but that's the important part here that the view model scope is of course such a supervisor scope because we don't want this behavior that if one curtin fails that all others will also fail and be canceled and the same is of course true for life cycle scope but it's just important to understand this if you implement your own curtin scopes and usually these are supervisor scopes but of course it's dependent on the context that doesn't mean that curtin's scope this one here is bad it's just that you need to understand the difference and if you want that behavior that if one curtain fails all of them fail for example if these jobs are kind of related and you usually want that then you of course need to use the curtin scope and not the supervisor scope so far so good as the last thing as i mentioned i want to talk about the common issue i see people do when it comes to exception handling in uh curating let's get rid of this again actually this whole thing and we again just launch yeah let's do lifecycle scope logic 13 in that and we launch a child curtin in here and let's say we do a network call here we just simulate a network call where where which could come from retrofit for example you might want to catch http exceptions if your network call failed if it responded with a 404 or any other type of network error so you say try and catch you catch these exceptions in here which you could print here and here you could just simulate making that call however if you now save that in a job and you cancel this after some time let's say we delayed this outer curtain um for 300 milliseconds and then we cancel it because yeah it could be that your user navigated away and the view model scope was cleared which would cancel the curtains in it or any other reason basically and here after that launch block finished we would write carotene 1 finished so this should actually only be called if that curtain actually succeeded because if it was cancelled then we would get a cancellation exception it would be propagated up to this one and it would actually also make this one fail here however if we now launch this and take a look in logcat then you will see we get our cancellation exception here but it still prints curoutine 1 finished so that's weird so even though we cancel the security in this launch block here after 300 milliseconds it will still print this here but shouldn't it actually not print this if the curtain was cancelled before and yeah it should let's think about why this happens and that is a very common mistake as i said what happens if this protein is cancelled if it's cancelled the current function that is suspending that specific creatine which in this case is delay will throw a cancellation exception but as you can see since that delay is executed in a trying catch block that cancellation exception would be eaten up by that try and catch block since we just catch general exceptions here we would simply print that exception that cancellation exception which also happens here in logcat but since it's since it's now a card exception and since we yeah kind of consumed that and eaten that up it's not properly propagated up anymore so this outer curtain scope doesn't know that this child croutine actually was cancelled and that is why it will still print this log statement here so let's think about how we can fix this on the one hand of course if let's say that is a retrofitted call again that gets cancelled you could do this by specifically catching by catching specific exceptions like an http exception here which we don't have here let's just take that one instead then that would of course not be called here for cancellation exceptions because you only catch these specific exceptions so if we now relaunch this take a look in logcat then you can see it's cancelled and we don't see our crew team 1 finished log another option which you could apply here is if you want to catch general exceptions is that you check if that exception is a cancellation exception and if so you would simply rethrow that exception so in that case you throw it again and that would also notify the parent scopes so if we now relaunch this we will see that it will just work the same as before but that is a thing that not many people talk about but it can really mess up your your core routines and it can lead to wasted resources because your protein that is actually cancelled will still do some things and will still consume some resources and that is really everything but obvious so that was really a deep dive into current teens and i hope that really helped some of you to understand them on a deeper level because i think the cotton docks aren't yeah are very cryptic on that so it's not obvious if you just read through these i hope that made it a little bit more understandable here if you actually want to take that to the next level and learn about flows then i have the perfect playlist here for you so definitely check that one out you
Info
Channel: Philipp Lackner
Views: 39,725
Rating: undefined out of 5
Keywords: android, tutorial, philip, philipp, filipp, filip, fillip, fillipp, phillipp, phillip, lackener, leckener, leckner, lackner, kotlin, mobile, coroutine, coroutines, async, flow, suspend, fun, coroutineexceptionhandler, context, try catch, cancellation, cancellationexception, throw, rethrow, mistake
Id: VWlwkqmTLHc
Channel Id: undefined
Length: 22min 13sec (1333 seconds)
Published: Sun Jul 10 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.