I'm using retrofit to perform two network calls as...
# coroutines
c
I'm using retrofit to perform two network calls async, and I then await their results. The thing is that I don't know how to do proper exception handling. I thought it would be straight forward, but apparently this isn't working. Is there something with async that I have to do differently?
Copy code
val work1 = async { getThingsDone(43) }
val work2 = async { getThingsDoneAgain(123) }

if (work1.await().isSuccessful && work2.await().isSuccessful) {
//do my thing
} else {
//failure message
}
Now with that block above I saw that I was sometimes getting some IOException, and so I wrapped it all in a try/catch.
Copy code
try {
val work1 = async { getThingsDone(43) }
val work2 = async { getThingsDoneAgain(123) }

if (work1.await().isSuccessful && work2.await().isSuccessful) {
//do my thing
} else {
//failure message
}
} catch (e: IOException){
//network failure message
}
But I still get the IOException. It's as if the try/catch doesn't work.
t
async is bound to your current
CoroutineScope
, so if it fails with an exception, so will your surrounding scope.
use:
Copy code
try {
    coroutineScope {
        val work1 = async { getThingsDone(43) }
        val work2 = async { getThingsDoneAgain(123) }
        [...]
    }
catch ([...]
o
that will still fail all the way up
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html for details, but basically when
async
gets cancelled, it must cancel the parent
you can do
supervisorScope
, rather than
coroutineScope
, to prevent it from cancelling all parent jobs
1
t
I just tried it, works as I described
c
try catch around the whole coroutine scope makes sense to me. It actually makes it easier in this case. I do find it weird that I can't try/catch in the middle of my coroutine scope though?
t
The new scope create with
coroutineScope
will be cancelled, but the builder function waits until the scope created by
coroutineScope
is done (exceptionally or not). The reason is simple: If you just do
Copy code
try {
    async { //stuff }
} catch (e: Exception) {
    [...]
}
The stuff in async can actually fail, after your current coroutine has left the try clause.
So to be constistent, a failing child-job of a normal CoroutineScope will kill all jobs of that scope. By opening a new scope and waiting for it to be done, you create a well defined control-flow where exception handling is possible
c
Thank you. It makes sense that I can just wrap my entire coroutine scope with a try/catch. Especially in this scenario. What would you say is my best option if I wanted to handle each one of my two async blocks with a separate try/catch? For example, they both should go off and async download stuff from the web, but it's okay if one fails, I'd like the other to complete. Any "easy" way to quickly switch over to that kind of behavior?
s
Maybe this'll help you figure out what happens: “Exceptional Exceptions for Coroutines made easy…?” by Anton Spaans https://link.medium.com/nuHcn7r6l2
t
Maybe something like:
Copy code
val downloadA = async {
            try {
                // download
            } catch (e: Exception) {
                null
            }
        }

        val downloadB = async {
            try {
                // download
            } catch (e: Exception) {
                null
            }
        }
        
        downloadA.await()?.let { value ->
            println("successfully downloaded $value")
        }

        downloadB.await()?.let { value ->
            println("successfully downloaded $value")
        }
?
👍 1
c
I put a try/catch around my scope and I still crash unfortunately. So now I have:
Copy code
try {
    viewLifecycleOwner.lifecycleScope.launch {

        val work1 = async { getThingsDone(43) }
        val work2 = async { getThingsDoneAgain(123) }
        if (work1.await().isSuccessful && work2.await().isSuccessful) {
            //do my thing
        } else {
            //failure message
        }

} catch (e: Exception) {
//show failure message
}
t
launch behaves pretty much like async. It's the same problem again. You will probably need to use the
coroutineScope
builder inside your
launch
which will create a new scope and wait for it (and all its children) to finish
and surround it with your try-catch
c
Oh wait!
viewLifecycleOwner.lifecycleScope.launch
isn't a "scope"?
a
Use awaitAll, it will resume with the first exception of any deferreds
t
awaitAll
will not be able to run, if a failing
async
kills your scope before.
I don't do android development, so I don't know what
viewLifecycleOwner.lifecycleScope.launch
is, but I assume it will concurrently start a coroutine so you have the same problem as before: If you try-catch around it, you are actually just try-catching the creation of the coroutine and then immediately proceeding in your code. Your started coroutine might fail 10 seconds later which you can't handle anymore.
coroutineScope
creates a new scope and waits for this scope and all it's children (e.g. all `launch`s and `async`s etc...) to complete. If any of these fails, the method will return and throw the causing exception.
a
True on scope kill. I expected the asyncs to be long running, which makes the possibility of that actually happening remote, but you could ofc lazy the coroutines async(start = CoroutineStart.LAZY), to be on the safe side.
t
But how do you detach the `async`s from the scope you launched them in?
a
Thats a good point, never thought of that.
Could use some kind of structure resembling this:
Copy code
fun main() = runBlocking {
    val list = runCatching {
        collectExternals()
    }.onFailure {
        it.printStackTrace()
    }.getOrElse { emptyList() }

    println(list)
}

suspend fun collectExternals(): List<String> = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
    val one = async(start = CoroutineStart.LAZY) { testFun() }
    val two = async(start = CoroutineStart.LAZY) { testFun() }

    awaitAll(one, two)
}

fun testFun(): String {
    Thread.sleep(1000)

    throw IllegalStateException()
}
Then you can "try catch on the coroutine", but if you want to keep any successfull results, youd have to catch the exception in the async, return a null and mapToNull or something like that.
s
About "detaching the asyncs". Look for "switching CoroutineScopes" in my article here: https://link.medium.com/A3QGyB2Jn2 Basically, use a different CoroutineScope for the 'detaching'
async
than the calling CoroutineScope.