Async-Await v/s withContext in Kotlin

Recently read an article here on HashNode on async/await v/s withContext, which you can check out here: https://effbada.hashnode.dev/settling-the-async-await-v-withcontext-debate-in-kotlin-coroutines-ckcmbjf4i000s8cs13w1t8w1m .

I wanted to share what I commented on the post itself. It can be confusing to devs who are just getting into coroutines and may end up using it incorrectly.

Coroutines are syntactic sugar to write non blocking code in a sequential looking fashion. They are akin to callbacks or futures/promises. That is first and foremost. The ability to schedule multiple things in parallel is a secondary benefit.

Use async/launch when you want to do work in true ‘parallel’ - async for when a value needs to be returned and launch for fire and forget style work.

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

1
2
3
4
5
6
7
8
9
suspend fun firstTask() {
delay(4500)
println("done task 1 on ${Thread.currentThread().name}")
}

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

This prints

1
2
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

1
2
3
4
5
fun main() = runBlocking {
withContext(Dispatchers.IO) { firstTask() }
withContext(Dispatchers.IO) { secondTask() }
return@runBlocking
}

this prints

1
2
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.

If you see yourself writing async-await-async-await - then you should reconsider refactoring to use withContext instead - it will also make testing easier since now its just a regular suspending function. If you are doing async-async-await-await that’s probably a much more proper use case of async.

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.