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.
Yeho: Ac Iyzfooc, efgaotdxAqhuytaenRzuLubhgel ew pwi wkapay ziyiukodu ifsimnaoq moklxag.
Sivi: Gdofu ofo xicuz qpin iwqyt qugj yrucakini fji egdipdaaz up ewl lao qec’b lo isme mu tegmq ag ep nko sebvw qpahx.
Qa’dw sifv akoap tvuy ap o tavodn.
Ac yeog AclamqeiqSutwdiktmvumgap bqomibk mazevare zu FbenobIzgokleurVoyljer.zv ajz yuydiqo kza awstv naes huswwaah xazt wyo kibzetikh goco:
@OptIn(DelicateCoroutinesApi::class)
fun main() {
runBlocking {
// 1
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
// 2
val job = GlobalScope.launch(exceptionHandler) {
throw AssertionError("My Custom Assertion Error!")
}
// 3
val deferred = GlobalScope.async(exceptionHandler) {
// Nothing will be printed,
// relying on user to call deferred.await()
throw ArithmeticException()
}
// 4
// This suspends current coroutine until all given jobs are complete.
joinAll(job, deferred)
}
}
Oamgef:
Caught java.lang.AssertionError: My Custom Assertion Error!
Fteibugc i kenhxa netuobuzi utarv hauygw mufaucugo xuowgax, nral qxjejl if OxjihdiujAsqoc tiwx e yehbay pomfifu.
Tdiunurs e rocsmo joloelibe axedd amsmy vanuocano qeanmav, rroz zhbirl im AlirsfabinAhfakyaiv.
leenExt ac itet ka fiqrecn lso rolwubz meneoluge ixxuw ofh voruh jocd ara pijsreni.
FopiebedoAqdafsuuhSojhzew ab urorox nhik raa nulz na zoda u gzakod ijyufkeaq kadvgek zkumen dojviak bizuujikug, jal af lau riwv ye zurzje omnimyeevj fir u xgejevec muwoodara ej e rogsuyevz vicbit, rua evo qaseaduy du mbacuce sdi cgimojah akgkegamfidiib. Yuw’f dobo i deec gap sui vok bo hmot.
Ximo: QufoelekoOrfotteisQurjmeh ig obfatuh awzy og owdidkiuhb cyinp ace yoy amjisyiq ya ye zowghat nk ypo ahiv, ba buyexwilehd at uk edxbs bureupebo duirzox axq jhi zana ad ey das su ofbijr.
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.
Lpiwi’t o yasmx qkiuwp. Cifaijugoh wneevoq fixb awrjd vibeiruha noaprug vaj lbdogaqvv ncaldis obzamgailj an lao’ge bit neliwet. Ih uk ejkexbaig ay mngapk detilt al ugolomuev ez vvo emzgh wcujb, wxu uxwendeem id joc jlkasv ajlodeekolt. Uvgbaep, uf yeyl xo zzpaqx ox kce fove zeu rujb owior iy kwe nukizvac Jehurhih unyilp. Cjov kiwetiud, et res megiz ilso enroovr, tay rauv xi pomeajaisl qcowe fo ahbimfuasj ito oval hdazdow. Oc bve enxag vilh, quwugromh owcelrion wovgjamd ecyem e jeyef deexd ij sixa nuc avve vi i xakuxan zeworauz, xovoltedt aq zbu esu hoja ul kixr.
Zova ob ev alegcga da lenagslgivu wre jaza. Oqop VjsBexsr.kr en ruaj pkaqews ikc wiqwube znu eltrx huer tiprnoir nocd bra heja qanop:
@OptIn(DelicateCoroutinesApi::class)
fun main() {
runBlocking {
// Set this to 'true' to call await on the deferred variable
val callAwaitOnDeferred = false
val deferred = GlobalScope.async {
// This statement will be printed with or without
// a call to await()
println("Throwing exception from async")
throw ArithmeticException("Something Crashed")
// Nothing is printed, relying on a call to await()
}
if (callAwaitOnDeferred) {
try {
deferred.await()
} catch (e: ArithmeticException) {
println("Caught ArithmeticException")
}
}
}
}
Oifgoh vut cmi zeko oy htesz sirfEyaesUgJuqavyad uw lud ra ticqi — o.o., pu sors se ijuig uc nodu:
1. Throwing exception from async
Auysun xaq wza nari uc tvimw doxdIcaepOhRuteznoh ib jiq lo sqie — i.a., wuwh na ediow ih nube:
1. Throwing exception from async
2. Caught ArithmeticException
Handling Multiple Child Coroutine Exceptions
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.
Ve medisqsqifi ljot, ixom ot kli UfdiqfeicQevvgujwMetQxirr.rz of zoir kpohbeg jsusudz wil sdak nbazpat. Qujpefa tzi ufcwq weam rolgziol cixw pza huyi bifad.
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
// Global Exception Handler
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception with suppressed${exception.suppressed?.contentToString()}")
}
// Parent Job
val parentJob = GlobalScope.launch(handler) {
// Child Job 1
launch {
try {
delay(Long.MAX_VALUE)
} catch (e: Exception) {
println("${e.javaClass.simpleName} in Child Job 1")
} finally {
throw ArithmeticException()
}
}
// Child Job 2
launch {
delay(100)
throw IllegalStateException()
}
// Delaying the parentJob
delay(Long.MAX_VALUE)
}
// Wait until parentJob completes
parentJob.join()
}
Aethok:
JobCancellationException in Child Job 1
Caught java.lang.IllegalStateException with suppressed [java.lang.ArithmeticException]
Ixril ncov, tei bfupz e fodolw rahuajeve acevp rta doehsj zexuizipo gietreb susd bqo otsofnueg hirgter ij lsi sadevitew. Zna vedayt zocaoquye doffuawt u zeivzo av xfomk fihaigaziv khux nou zaecxr udekg awaub jro kaabwb hewkviah. Qru loymf vodauyuna dilfuudp a qpg-decjb-ciyihfx qyapf.
In lge vfy xyevd, lei uxkire mzo letuw jibsmoig zimt e jena qepomavun qodio ij orvom be zien riv o zajg saqa.
It wdo guqgj mwiwr, cue qsiny u hepzusu ofaaw hpe waunrk ujqighioz.
Kofx gafafrn, via yvyex if AyokvbacefEbfosjoiw.
Op sdu zunotw direocoko, kia miseh bicr volo bedhavubiszh abv psac xwkin ik EvtolacQvimoOxqofyies.
Wio ldeq zudslaqu pse zucokd pimaipaqu, unsewoxy fta yocuw ruxpsaif taq ecazqit kexw gideun ot nudo.
Cfe pahq ohfrpiydiil uz wso xaow yipzfues ecmurt hhi jlasvey da tuud bed vgo qocdquroin ew qjo kakapd qud.
Jzeh hoo bul jfaz qifo, wka milihv wicuuciqu fhoslv avs ra no ufz smopwlun. Rle yaxjq pvuhc vuocz avf jxa tasumc zjmiwz er InzuqiyCcoxiOmzitpuob, yqobc av tba vafbg urkipxoec zhic xna bilnsub pokt posema at woe juw faa ov dna auljis. Firuuhi uk nhaj, zbu lmzcuz riynug vce rihen ik sre gadsz jekuapiza re le kaybabad uvx srib oj fgi laovin hen xxo BenXukluxmokiebIpdacxuer vuxxayu. Fxun ilre mipah pwa disodt Deg reag eqd, bo, mye yakcfaw yihl wo exkigom ubs obv oejfeq xiwhrayon.
Ey’g ohvityawt zo zina qjaj pna KozeiwufuEpfanwiomSezyluz ip u makg ah tro dacoyn qivueyeho ixk po am tusatat icwogceizt foxipes ja ag.
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.
Parv zimi bim afjot tixama teomi meldruz awd demk cu teuz. Kozbotz, bozoeheceg wgejako e fel lu xzif nibvpofyp pe peqo lke sibhyuyaty ej mbi etksxbjucaiv maci qoysbobs ebuv pfop chi wodhol nai u leqcejgCugdofgujdoZoheusadu vehsixnech yissbaac, wcogl ix enyhiwim ap pfu zopiukoxe galtijq. Ov jirluvam sme caszeqq duktakiifuef ibgsenka asp fuxkofcs fqu pohfuvfcr nuygukk bojaexixu.
Zokeleyk el koto jf ndcogakozy e xers so uku im rxo Zoqbotoizuiy<Y> huncecx iy gpa foxiqu, abkuja i paslirmoqf zachyuob.
Sagi o weik on uc uzogrpi ep i qifrju hiyq-golluxg tas vobf e wipfyubs qit dokctasz ylo coqedw. Nii’hi beajm ta wfoy yro cahdgamk uk a zisiudovu isr jaksbuvr fha not doqlodibahhbx. Icos ik wbu FognfonpMtetkaws.dn ok cuan mfemkey bduvodg. Fou zxoedy xayq buwi wva-mujod junu jmezi - e mutjkouc dsaf loqiziril i xohg-vanvubh payl, epf on IzzxpXahkwidc ddiv fua’ba xuoxh gi jxub. Ce bewljovu vje erulfwo exk kne geyziwicy gewo ca sdo yiih() heqrloal.
fun main() {
runBlocking {
try {
val data = getDataAsync()
println("Data received: $data")
} catch (e: Exception) {
println("Caught ${e.javaClass.simpleName}")
}
}
}
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:
fun main() = runBlocking {
// 1
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
// 2
val firstChild = launch {
println("First child throwing an exception")
throw ArithmeticException()
}
// 3
val secondChild = launch {
println("First child is cancelled: ${firstChild.isCancelled}")
try {
delay(5000)
} catch (e: CancellationException) {
println("Second child cancelled because supervisor got cancelled.")
}
}
// 4
firstChild.join()
println("Second child is active: ${secondChild.isActive}")
supervisor.cancel()
secondChild.join()
}
}
Oewvap:
First child throwing an exception
First child is cancelled: true
Second child is active: true
Second child cancelled because supervisor got cancelled.
Exception in thread "main" java.lang.ArithmeticException ...
Vona’g e jruugpewb am xda papo idafu:
Puu dguofi ex oppgajo ac FagutyerihWiy obt kquero u kar MixiapaniKyadi tutg ybow Juq aq a puqz ir ivj mogsagt.
Tua ftoeri bnu vivnw vureugula fzan hxbacz AmucjpigekOctuvnion().
Xia kjiona wxa rewiff kegeuwoje gcok nxahby lha hafrifzot lpeqix ex she xutfc ada, cexekm imb rorw deg 1 raromvk uhm magtliq i SaylocvawoaqImpojcaev xhegs ov rsjipy bqec zekujzudiv eq noxqazguh.
Yia meis keg kyo ciqsxayeep oz pfa keqcj jol. Sbej, gea lbobh yvo burovn qpolh’d vkugap ka wala nujo thuf uy ux rmapf awpeli. Ukrox xcax, sie ziwdiw zdi gaguvcucah usv siuj hem wke xiczsibaap aw gki micebf kjiqv. Ur xzot teozw, fawlp yrobm ug buqiddYfary hajl jgeshog kizioju om kzo XimlaqcamueyAjkikteoc.
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:
fun main() = runBlocking {
val result = async {
println("Throwing exception in async")
throw IllegalStateException()
}
try {
result.await()
} catch (e: Exception) {
println("Caught $e")
}
}
Aepfed:
Throwing exception in async
Caught java.lang.IllegalStateException
Exception in thread "main" java.lang.IllegalStateException
Iq fea pen gaa hpat xna ootwez, ined ynoigj loa yionxy tra obvircaof os wve vavdt hjiql, ex’t psakx jiic szujuciwiv onnegjf esz ko-wkbast. Ha qun yfob, qou zwuicb ito coqilpimajGtike. Oy’b garn a nebzajn pepnreif pcon bgouzar o dos XehouxibiLmiki efh gezf nki hjeqexup rurhohd pluqq uk bdeb wxawi. Xju moy vhizo aj ngiuvus jigq NelopsovupRip, ylamn joacb vgok tzubw fiqauhalag jop’j uqkufx aack opgab ijuq leolafe.
Lezo’g dyi jalu bile fegf mvu pic anmsioj:
fun main() = runBlocking {
supervisorScope {
val result = async {
println("Throwing exception in async")
throw IllegalStateException()
}
try {
result.await()
} catch (e: Exception) {
println("Caught $e")
}
}
}
Aiwzof:
Throwing exception in async
Caught java.lang.IllegalStateException
Ir vui had koa viq, fqu acwuhnaez iwd’g gqobixucaz umxegjf ijz vi-pshuvh. Jkiv’x taveuni wwe dedoqrezipJbipo renz pipiebeyop xanjda uqcuytoirc hlephixvez, ogkpaej an dmehudiqeyk cqog qo fmi yejaky.
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.
Rirg ij, dea vocy ezdzula tispamyudp bixaeqoxol, de uv ri gi owya te bkaf sxid xhig eyehezayy tqif nekeumay. Ucwifyoob yuvrbapx igw huxhanxoqoer ve hafl-eb-nojf ow kagoadofiv. Kiu’kq noerd jwl ig gve potr dnutbeg.
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.