Settling the Async-Await v withContext debate in Kotlin Coroutines

Subscribe to my newsletter and never miss my upcoming articles

Asynchronous programming is a new reality in programming that we (developers) have to understand. To this end, Kotlin came up with a great library in the form of Coroutines. However, there have been debates as to the patterns to use and what works and does not. One such debate is the Async-Await v withContext to perform background work. So what are these two concepts and which one should you opt for?

async-await

To understand any code concepts, it is better to dive into code examples and then see an explanation. Let us say that we have two long-running operations as shown below and we want to use the results of those tasks.

private suspend fun operationOne(): Int {
    delay(1000)
    return 20
}

private suspend fun operationTwo(): Int {
    delay(6000)
    return 35
}

The first operation will take a second while the second one will take a whole six seconds. The idea here is to show the varying amounts of time that background operations can take. If you want to use the results of those values with Coroutines, you do the following:

fun main() = runBlocking {
    val opOne = async { operationOne() }.await()
    val opTwo = async { operationTwo() }.await()
    println("The multiplied result is ${opOne * opTwo}")
}
//will print
The multiplied result is 700

Here, we are using the async block, which returns Deferred<T> that has the handy await() method. According to the documentation, this method

Awaits for completion of this value without blocking a thread and resumes when the deferred computation is complete, returning the resulting value or throwing the corresponding exception if the deferred was cancelled

Deferred is actually a type of job that we can wait on its result before doing stuff. So, once we call await(), we wait for that operation to complete. Finally, we multiply the values and print out the results. To improve the readability, you can print out information on what is happening like the following

fun main() = runBlocking {
    println("I am working")
    val opOne = async { operationOne() }.await()
    val opTwo = async { operationTwo() }.await()
    println("Done working.")
    println("The multiplied result is ${opOne * opTwo}")
}
//will print
I am working
Done working.
The multiplied result is 700

On a higher level, launch and async are the same thing. However, the former does not have a resulting value and returns a job. On the other hand, async returns Deferred<T>, which is also a type of job so it can be cancelled if needed.

Note: Async can take a context where you can choose the Dispatcher you want to use.

withContext

This will also work if you want to do long running tasks in the background and do something with the result. The computation of your code will happen line by line of course. Using our previous example, the main function will become

fun main() = runBlocking {
    println("I am working")
    val opOne = withContext(IO) { operationOneWithContext() }
    val opTwo = withContext(IO) { operationTwowithContext() }
    println("Done working.")
    println("The multiplied result is ${opOne * opTwo}")
}
//will print
I am working
Done working.
The multiplied result is 700

As you can see, the same result is printed. However, withContext needs a mandatory context to work (Dispatcher.IO in this case).

So what should you use?

If you have upgraded your Kotlin version, then this question has already been answered for you. I am not exactly sure when this was done but this post shows that it happened as early as Kotlin 1.2.50. The it, in this case, is a lint that checks whether you are using async-await and suggests a simplification. The exact message you get is

Redundant 'async' call may be reduced to 'kotlinx.coroutines.withContext'

So just use withContext to remove that annoying lint check unless you need actual concurrency and not a simple operation. In most cases, withContext will do.

Happy coding!

PS: Let us connect on GitHub , Twitter , and LinkedIn .

Comments (1)

Asad Awadia's photo

Hey - nice to see another kotlin-er around here.

I want to add a bit more to anyone reading since this is a confusing topic. It's more than just a lint issue.

The reason the output is the same above is the same in both scenarios is the same because the .await() is done inline on those async calls.

Use async when you want to do work in true 'parallell'

Use withContext when you want to move work to a different dispatcher - this comes in handy when you need to move blocking work off the main thread. The two common dispatchers are IO and default. Use the IO dispatcher when you are doing things like network calls and default when you are doing compute style work [computing Fibonacci].

Take the following two suspend functions

suspend fun firstTask() {
  delay(4500)
  println("done task 1 on ${Thread.currentThread().name}")
}

suspend fun secondTask() {
  delay(3000)
  println("done secondTask ${Thread.currentThread().name}")
}
fun main() = runBlocking {
  async { firstTask() }
  async { secondTask() }
  return@runBlocking
}

This prints

done secondTask main
done task 1 on main

Because the two tasks are running in true parallel fashion - and the second task finishes before the first task - and both are working on the main thread.

Now let's look at what happens when you use withContext

fun main() = runBlocking {
  withContext(Dispatchers.IO) { firstTask() }
  withContext(Dispatchers.IO) { secondTask() }
  return@runBlocking
}

this prints

done task 1 on DefaultDispatcher-worker-1
done secondTask DefaultDispatcher-worker-1

You can see that this is still sequential [coroutines are always sequential by default] and that task 1 finishes finishes before task 2 but what we have done here is offloaded the work off of the main thread onto the IO thread pool - so in essence we have made the code non blocking.

This is the key - Non blocking code is not the same as parallel code. Some like to call it concurrency is not parallelism. Same idea - different lingo.