Capturing the return value of three different "Async" functions

Let’s say I have three functions

// The result type of somethingUsefulOneAsync is Deferred<Int>
fun somethingUsefulOneAsync() = GlobalScope.async {
    delay(1000L)
    return 13
}

// The result type of somethingUsefulTwoAsync is Deferred<String>
fun somethingUsefulTwoAsync() = GlobalScope.async {
    delay(1000L)
    return "Hello"
}

// The result type of somethingUsefulThreeAsync is Deferred<Long>
fun somethingUsefulThreeAsync() = GlobalScope.async {
    delay(1000L)
    return 30L
}

Is there any way to execute these functions concurrently AND assign the result of each function to a separate variable?
Something like this

var a: Int
var b: String
var c: Long
a, b, c = (somethingUsefulOneAsync, somethingUsefulTwoAsync, somethingUsefulThreeAsync).awaitAll()

I know it’s possible for multiple functions with the same return type, just not sure how I’d do it when the return types are different.
For e.g. concurrently making the call to several third party APIs over the network. Each one will have a different response structure

Just launch them and then await each independently.

val oneAsync = somethingUsefulOneAsync()
val twoAsync = somethingUsefulTwoAsync()
val threeAsync = somethingUsefulThreeAsync() 
var a: Int = oneAsync.await()
var b: String = twoAsync.await()
var c: Long = threeAsync.await()

Note: your example uses GlobalSclope. From GlobalSclope docs:

Application code usually should use an application-defined CoroutineScope. Using async or launch on the instance of GlobalScope is highly discouraged.

I recommend reading up on Structured Concurrency

Isn’t “awaiting each one independently” different from using awaitAll? Based on the documentation for awaitAll:

awaitAll is not equivalent to deferreds.map { it.await() } which fails only when it sequentially gets to wait for the failing deferred, while this awaitAll fails immediately as soon as any of the deferreds fail.

I used GlobalSclope for illustrative purposes only, those methods are copied over as-is from here

How about val (a, b, c) = awaitAll(somethingUsefulOneAsync(), somethingUsefulTwoAsync(), somethingUsefulThreeAsync())?

If you want immediate detection of failure, yes, use awaitAll. To extract the result in a type-safe manner, extract separately.

val oneAsync = somethingUsefulOneAsync()
val twoAsync = somethingUsefulTwoAsync()
val threeAsync = somethingUsefulThreeAsync()
awaitAll(oneAsync, twoAsync, threeAsync)
var a: Int = oneAsync.await()
var b: String = twoAsync.await()
var c: Long = threeAsync.await()
1 Like

You shouldn’t need awaitAll if you are using structured concurrency. The failed child will cancel the parent which will then cancel all its children.

2 Likes

Yes, I was trying to avoid awaiting each one individually. This pattern doesn’t let you “fail fast”.

Scenario

  • Let’s say you make three concurrent calls over the network. The first call is the longest and the third call fails instantly.
  • If you call await on each method, the earliest I can know about failure is equal to the time it takes for the first call to finish.

Other use cases

  • Fast failure is just one of many possible use cases.
  • Let’s say I only want to wait as long as I get at least one response?
  • This is possible in scala by using the select composer. More info here

select returns a Future that is complete when the first of the given Future s complete.
It returns that Future together with a Seq containing the remaining uncompleted Futures.

  • And there are other composers too, like: join and collect
  • The different is that the scala let’s you compose Future/Promises directly and I believe this is not a recommended practice in Kotlin (the equivalent would be a Deferred I guess).

I’m just trying to find out if I can do all these things in kotlin without having to write functions that return a Deferred

Read the bottom half of that page where it shows how to use async properly within a the context of a coroutineScope call.

Not when utilizing structured concurrency. If they are sibling jobs and the parent isn’t a supervisor job, then one failure will immediately cancel all the others.

You get “fail fast” for free when using structured concurrency.

Kotlin’s select method supports the same use case, though it’s more general. I prefer using Flow instead of Deferred in more complicated cases where I’m combining async data.

Kotlin has a variety of operators for combining async data when using Flow.

Code written using suspend methods is just so much easier to read.

If you do write methods that start new coroutines, take a CoroutineScope as the receiver or a parameter and use that scope to launch the new coroutines. This gives you all the benefits of structured coroutines.

Yes, you can, it’s just a new set of tools.
Here’s some tips to get you started:

  • Want an async result? Write a suspend method.
  • Want to queue async data? Use Channel.
  • Want an expression of concurrently determined values? Use coroutineScope with launch/async inside.
  • Want a stream of async data (especially if processing or combining that data)? use Flow.
1 Like

Thanks for the suggestion Nick, I’ll explore channels and flows.