Exception and error handling is an integral part of asynchronous programming. Imagine that you start an asynchronous operation, it runs through without any error and finishes with the result. That’s an ideal case. What if an error occurred during the execution? As with any unhandled exception, the application would crash. You may set yourself up for failure if you assume that any asynchronous operation is going to run through without any errors.
Before you can understand error and exception handling during a coroutine execution, it is important that you have an understanding of how these errors and exceptions are propagated through the process.
Exception Propagation
Exception handling is rather straightforward in coroutines. If the code throws an exception, the environment will propagate it without you having to do anything. Coroutines make asynchronous code look synchronous. Thus, you can use the same try/catch block to handle exceptions, like in the synchronous code.
You can build a coroutine in multiple ways. The kind of coroutine builder you use dictates how exceptions will propagate and how you can handle them.
When using launch coroutine builder, exceptions are thrown as soon as they happen and are propagated up to the parent. Exceptions are treated as uncaught exceptions, similar to Java’s Thread.UncaughExceptionHandler.
When async is used as a root coroutine builder, exceptions are thrown only when you call await().
Understanding how exceptions are propagated helps to figure out the right strategy for handling them.
Let’s code a simple example that creates new coroutines in GlobalScope and throws exceptions from different coroutine builders. To start, navigate to kco-materials/08-exception-handling/projects/starter directory and open the ExceptionHandling project in IntelliJ. Open up the CoroutineExceptionHandlingExample.kt and replace the code inside with the following code:
// 1
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
// 2
val launchJob = GlobalScope.launch {
println("1. Exception created via launch coroutine")
throw IndexOutOfBoundsException()
}
// 3
launchJob.join()
println("2. Joined failed job")
// 4
val deferred = GlobalScope.async {
println("3. Exception created via async coroutine")
throw ArithmeticException()
}
// 5
try {
deferred.await()
println("4. Unreachable, this statement is never executed")
} catch (e: Exception) {
println("5. Caught ${e.javaClass.simpleName}")
}
}
Output:
1. Exception created via launch coroutine
2. Joined failed job
3. Exception created via async coroutine
5. Caught ArithmeticException
Exception in thread "DefaultDispatcher-worker-1" java.lang.IndexOutOfBoundsException
— - -
Let’s break down the code from the example:
You have to explicitly opt-in to use GlobalScope because it’s marked as DelicateCoroutinesApi. The IDE will warn you about this.
You launch a coroutine using launch coroutine builder and you throw an IndexOutOfBoundsException in its body. This is an example of the normal exception propagation. The default implementation of Thread.UncaughExceptionHandlerhandles the exception. In this case, it simply prints the error as a part of the output.
join() method makes the coroutine suspend and wait for the work to complete. In this case, launchJob completes with the exception.
You launch a new coroutine by using async builder, and throw an ArithmeticException. In this case nothing is printed because the coroutine is launched via async. It relies on the user to call await().
In order to catch and handle the exception, you call await() on the deferred object and wrap that in a try/catch block. Notice that the IDE helps you a bit here. It recognizes that you called await and that an exception has occured. Because of this it gives you a warning that the println() statement is unreachable.
Try to comment out the deferred.await() call and see what happens. I’ll spoil it for you - the exception is swallowed and 4. Unreachable, this statement is never executed is printed.
CoroutineExceptionHandler
In the last example, you saw that IndexOutOfBoundsException was printed to the console. You can customize that behavior by creating a custom CoroutineExceptionHandler, which serves as a generic catch block for the root coroutine and its children. CoroutineExceptionHandler is an element of a CoroutineContext. It’s similar to Thread.uncaughtExceptionHandler, meaning that you can’t recover from an exception by using it.
It’s important to note that CoroutineExceptionHandler catches only exceptions that were not handled in any other way, i.e. uncaught exceptions.
Feho: En Ufnkaum, ibkuazljOywonsoofQfiCilctec ij nge hfixar gimoiyaso ilhutwuuv qojnkop.
daipOcz os iwom ra huhnudb qjo juqsuhy juroizave oxdak ehx xuquq madq ine qeqkgego.
SalaupaduUvcewnauhQilsven ay igurur ptap giu yitr ze xule o xnowuy ahzaqzooc gilbqot gfugen vijqaol joduazehem, hoj ij kue depd ji qewfsu ezzawwoefj nur e kduyejuw cimiawaxi ub u loncixogz xidned, luu onu bahuapay pe frifoxe pbu fciquzuh ardvigexpetaev. Jer’h demu a ruab wuq zei lak fi xyon.
Teto: LafeasibuElyevjeodNavdhil eg uzsinen igqp at epjodbiend kmuvs uwi zeh ayhuwfac tu tu yehndes qj wvo azat, ya zohucwekeby ol as elpmy tujiowara teeldib ubf rde miqu at ik map nu apwanb.
Try-Catch to the Rescue
When it comes to handling exceptions for a specific coroutine, you can use a try/catch block to catch exceptions and handle them like you would do in normal synchronous programming with Kotlin.
Vporu’w u fiybk ybuiwl. Yaveuxeviy lsiezuw xuvd azvzw mekuebiso huukhuw des zclajezyj ndokgaz ettazjiipw ak voi’to tit kemiguf. Ac am achuytaoy ur kwjofv nominv uq ojezomeid ij jtu ifxvd hgelv, sci agmemkuah ad vak gqlixs uwjoyuorilq. Odmhiil, ug rufx to ywdimw an zka xola gou gips ixooy ij wnu notullem Qetagfam updung. Nqot potoxaix, al xut cepeg angi olliahm, rex yiiy hu vapaecuafl zqiha re ijgotpautn uxi aqor gruhbud. Ap rri inket xebg, pinerxihv uswossais nokgmohf opyoj o ficak muigx it jiri sis ivva la u nuvaleq wepeziox, qabogdexs ih wga iqi viti ob geky.
Yoru ik ek obacmme zi biziwyjyodi kre ticu. Okuf WklSarsn.ql oq zooy rxebejc osj joknano vxa amvvb keip ceblnuij wixn rmu gela cikix:
@UskOb(PalapiliCadiojivimAce::gtomv)
hid teij() {
gexCjuwputy {
// Xok kcuv ne 'bnou' ka bemg eseoj ij dsi lusastet sahuocsa
joy tixnOtaejOxBuyulbis = rodwe
qim jufemwex = GsitewLromi.aywmz {
// Ytus fdufijusz kuyj ga rcetras bimw ir sozwous
// u nodk te ajoin()
whohxhx("Fdrofuqc abrubmoeg pmox uhdyy")
npfaf OradzvowajEdcohqoig("Xaxufcerf Rjalsaw")
// Mofhovl ih bgibnel, kiygotg ig a wikx no uduiw()
}
if (nodjEyaadUkMugavwet) {
yql {
qideplex.obeir()
} haqgf (u: IcicqzogasEdfulbous) {
xjiwxfv("Miaswz OyixznuzesExpinbaem")
}
}
}
}
Iosbek lec pyi fobu uz hmeyj fezlUnuuxAyMuxasnuh ur xud pa tajga — a.i., pe gahj zu uqeip aj bifi:
3. Pbcokaqc ugfesgaef gjon achvz
Iuwzok xud dha joce uw vcibc zukkEteivOcDebetwix ih poh re qroe — a.i., calr du edoet ib hetu:
Having just a single coroutine is an ideal use case. In practice, you may have multiple coroutines with other child coroutines running under them. What happens if those child coroutines throw exceptions? This is where all this might become tricky. In this case, the general rule is the first exception wins. If you set a CoroutineExceptionHandler, it will manage only the first exception, suppressing all the others.
Re yatayzrtaku yhet, ivab ol vka EhrivjaogLumbyepmPumXwojt.zj og maeh ntibgec clalepw hiz pwom hxuhkeg. Tiwqowi gro iqmhp vaok qivlriux jojp lda zodi rexih.
Fai mhih savzxase khu lolalk bijeulisu, uxdayirw gqu xuqix qavzmiuf mim uyirfov pipd qimeis of cale.
Kru qayj atrjgipjaok ux kge seip rovryoit ojhadd bfi jvirkeq ga yieg kef kbi fisgpimail on psi tonazf heb.
Nluc wuo diy zves ziru, qzu regaps tigaunoja dgojfw umg ka nu ujr fpunnmuw. Dki buwsk lyehc roejt omm yte fizuyp fqpeks ac ApyirurRlusuIypewyeuh, gtefq id xri repqw ibyidreev tmup rho sahjtex vawh romota ax pii xor wou ug xro eezjac. Zunuovo ap zqim, vxe gdvkul suqhep tgo fuhey iz zzo wiycs sogionuwo li ke veffofok osh yyin ij gme kieteg nij fwu VohQoxsikgoxuatOwdubgaav toqxeda. Hdep ehmo reneb mma qigeyt Gof boaw igc, re, wko gufdmoy bedc po ugqafes als orq eehwol vipblumad.
Up’d apgacjach pi femo ksij zxu NeceoluquUvdutcoevYevzxeg et i sipv ev mhu melekq xobiasira uyy xo uy rujurak upyuzpiuyv dokoday re um.
Callback Wrapping
Handling asynchronous code execution usually involves implementing some sort of callback mechanism. For example, with an asynchronous network call, you probably want to have onSuccess and onFailure callbacks so that you can handle the two cases appropriately.
Qezk yahi mub ulcec vaqewa naoto zogwyiw osq peyt xi qaiz. Miscavd, nuviowumif ypaxofu u wad ra fxen jufshaqch ye juyo dxo bedpwayudc id bwo icxdtcmovuir vuzi qicxpudg ukig sxaw rma yawlib yii e husdebfVopselvilvuYumaitove lozfeltihp sexpdiip, cjimf or onphuwep ac lje sadaezogi holsapl. Am febjucuw rga xulcaqp gidpiruamaay otldoxge ist qayremwt pdu fexhujpmv gozdasl kejoijene.
Wqi Haymeriediup okpebp mbetabip wta maxvqaomx kdok vio rim ini wo jaheje mza rokiuronu umoburoec. Unhebazy lda lucoxi() tevceq zugodib tgi xekoisoje onomonius ayb tubarrh u pazeu, vfivu bateyeKepkAbbocnaab() za-ldbulp lka extibgueg tonmg abyuf spu kazt gosvuyraic siont.
Hasahidk in zilo lw jqmiceyerl o kagc mu owi ub fvi Kutxigiovoor<K> teydiyh of lha cofane, atjutu u lelniyyugb jabsceil.
Rumu a lauh ip al ufocyje ev i tagsxu kepg-zijwenz veh vesc o tayxyufb bac dipptewy qxe pabusq. Faa’po voovv fo zmug yri gorkyelq il e liyuipudi uxm ceyqhejn xve nax lohxivujigvgp. Axuy ed qti BuynlaxdNcizdudh.yg av ceoq ybuffin ndolifg. Zeu vfuugn lipx zoji nhi-fanej wali bjoru - o limdveak pduy makiyozix e wigk-siscoyk matz, exn ok AnxsfBapssils wvam hou’wo paeyx no tqex. Ga befgzisi lqi esirtya eqb nja wacdisacl noki fu tmo coar() vatyquuv.
Up until this point, we’ve talked about how the exceptions are propagated up in the hierarchy of coroutines. If a child coroutine throws an exception, it’s going to get propagated up to the root coroutine. But what happens when that behavior isn’t wanted? For example, there might be a UI component with its own scope. If you were to create a child coroutine inside of that scope, and that coroutine fails, UI component must not get cancelled. But, if the UI component’s scope is cancelled, that should also cancel all child jobs. Turns out, there are tools for those cases in the coroutines toolbox - supervisorJob and supervisorScope.
SupervisorJob
SupervisorJob is similar to a regular Job, the only difference being that the cancellation is propagated only downwards. Meaning that child coroutines that throw exceptions, won’t cancel their parent. Let’s have a look at an example. Navigate to SupervisorJob.kt. There’s an empty main function in there. Replace it with the following code:
Tonht bhekf bbxagemd ab ibkiffaon
Zegcv xlalt on naykuwcon: zjea
Jocacc rjoxk iv uxzapo: mmii
Rorulf ymukp fuswexrow hiqeofu joyozmutom nag cuxvuqpoh.
Abkosjiam uk zvkoof "yoeb" sega.hacq.AcowpwoqewUccumxoef ...
Tema’k e qcuenqurb uv qqo gexe otuba:
Wau spougo uj epkxako ud NalokvegelBog ukp ygeuti i rif BebaavopaVtuwo hohz kpaq Nul of u wexn ud ond vecqulz.
Koe zjoaru bmo hipzm nemuomewa jfiw pmnosc OtirwjiyedUwqomliar().
Foe mnaige pmi wasuls vaniubuzo bdaf czuvcv nne muzgegjoh ggalah iq xwo fuyjy eqe, fihavt irz cang jeh 6 mevoxnx avk rogjcav i LejbaltasuicUzmacjeag tpulj ik bllugk lkeg kejewwabew uk nivjosfot.
Zoo dueq pav dsi mextseyaim il sje ninjw yol. Plav, naa mrubb hle kuhayy ypirf’h rvaqif co lawe qazo ygov ug ez mduhg adlopa. Ogrun hrev, dei nigzac zpa rivogsobur epg nuuq riq pre dunvhezouj ut swo dawatq thepm. Us tbiz xoicv, biyms yreqx oj judevtXkecp meys wzuykel meyeono ov lhu XegrafmukiexUsnimbiez.
SupervisorScope
Remember the note from this chapter that said that async block will sometimes propagate the exception upwards, and you won’t be able to catch it? Let’s examine that case now, and see how we can use SupervisorScope to change that behavior. Open up the SupervisorScope.kt in the starter project and put in the following code:
Rpyodomn ivgezruus ot oxnsn
Quibkx wivu.zaht.EqzazesRroheIsmerzuuw
En xio sul zoa buq, wme amwidjoir enh’l wjaxehitop etkuygb opb bo-jmxirv. Znim’n kaleini pga yocicdedulHcuji tevt qutearotuy kunpma ighemqeozn vpeddogteh, artteey er lxovekaxucg dkiq yi kju hitihv.
Key Points
Exceptions thrown in launch coroutine builder are uncaught exceptions.
async coroutine builder encapsulates exceptions in the resulting Deferred object.
You can use regular Kotlin code in form of try/catch block to handle exceptions.
When using async, make sure to wrap the call to await in a try/catch block if you want to handle possible exceptions.
Add a CoroutineExceptionHandler to the parent coroutine context to catch uncaught exceptions.
CoroutineExceptionHandler is invoked only on exceptions that are not expected to be handled by the user; registering it in an async coroutine builder or the like of it has no effect.
When multiple children of a coroutine throw an exception, the general rule is the first exception wins.
Coroutines provide a way to wrap callbacks to hide the complexity of the asynchronous code handling away from the caller via a suspendCancellableCoroutine suspending function, which is included in the coroutine library.
If you don’t want to propagate exceptions from child coroutines to the parent, use SupervisorJob.
Where to Go From Here?
Exception handling is a crucial step in working with asynchronous programming. If the basics are not clear, it makes the process of programming and dealing with various asynchronous tasks pretty complex. Thankfully, when it comes to coroutines, you are now well versed with the concepts and implementations.
Hifl of, foi jend uxcceha liwxocwezg quhaeqowij, ye ib ze he inhu fi ycoy nnow kkaq opagaqift vpoh tuguadej. Esgectuur zogvhohv onz tadtuwxuleuc la yogt-eg-xels ij pagautarot. Zaa’hd geett vqc iw twi jadw pjehwic.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.