https://kotlinlang.org logo
#coroutines
Title
# coroutines
c

Colton Idle

12/12/2019, 9:31 AM
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

trathschlag

12/12/2019, 9:36 AM
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

octylFractal

12/12/2019, 9:39 AM
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

trathschlag

12/12/2019, 9:42 AM
I just tried it, works as I described
c

Colton Idle

12/12/2019, 9:42 AM
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

trathschlag

12/12/2019, 9:45 AM
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

Colton Idle

12/12/2019, 9:54 AM
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

streetsofboston

12/12/2019, 9:54 AM
Maybe this'll help you figure out what happens: “Exceptional Exceptions for Coroutines made easy…?” by Anton Spaans https://link.medium.com/nuHcn7r6l2
t

trathschlag

12/12/2019, 9:58 AM
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

Colton Idle

12/12/2019, 9:59 AM
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

trathschlag

12/12/2019, 10:02 AM
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

Colton Idle

12/12/2019, 10:05 AM
Oh wait!
viewLifecycleOwner.lifecycleScope.launch
isn't a "scope"?
a

Anders Mikkelsen

12/12/2019, 10:20 AM
Use awaitAll, it will resume with the first exception of any deferreds
t

trathschlag

12/13/2019, 8:42 AM
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

Anders Mikkelsen

12/13/2019, 8:48 AM
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

trathschlag

12/13/2019, 8:50 AM
But how do you detach the `async`s from the scope you launched them in?
a

Anders Mikkelsen

12/13/2019, 9:01 AM
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

streetsofboston

12/13/2019, 9:37 AM
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.
9 Views