How to make sense of Kotlin coroutines
Coroutines are a great way to write asynchronous code that is perfectly readable and maintainable. Kotlin provides the building block of asynchronous programming with a single language construct: the suspend
keyword, along with a bunch of library functions that make it shine.
In this post, I’ll try to explain in simple words the basics of coroutines and suspending functions. In order to keep it short, I wont dive into advanced constructs based on coroutines. The point is rather to give an overview and share my mental model of coroutines.
What is a coroutine?
The Kotlin team defines coroutines as “lightweight threads”. They are sort of tasks that the actual threads can execute. The banner on Kotlinlang.org is an illustration of this :

The most interesting thing is that a thread can stop executing a coroutine at some specific “suspension points”, and go do some other work. It can resume executing the coroutine later on, or another thread could even take over.
So, to be more accurate, one coroutine is not exactly one “task” but rather a sequence of “sub-tasks” to execute in a specific, guaranteed order. Even if the code seems to be in one sequential block, each call to a suspending function delimits the start of a new “sub-task” within the coroutine.
This brings us to the meat of the subject: suspending functions.
Suspending functions
You may find functions like kotlinx’s delay
or Ktor’s HttpClient.post
that need to wait for something or do intensive work before returning, and are marked with the suspend
keyword.
suspend fun delay(timeMillis: Long) {...}suspend fun someNetworkCallReturningValue(): SomeType {
...
}
These are called suspending functions. As we’ve just seen:
Suspending functions may suspend the execution of the current coroutine without blocking the current thread.
This means that the code you are looking at might stop executing at the moment it calls a suspending function, and will resume at some later time. However, it doesn’t say anything about what the current thread will do in the meantime.
It might go back to executing another coroutine at that point, and it could later resume executing the coroutine we left. All of this is controlled by how your suspending function is called by the non-suspending functions world, but there is nothing asynchronous about suspending functions from the user's perspective.
Suspending functions are only asynchronous if they are explicitly used as such.
We will see this later on. But for now, you can simply consider suspending functions as special functions that declare they take some time. And keep in mind that calling them implicitly splits your functions into sub-tasks, without worrying yet about the intricacies of threads and dispatching. That’s actually why they’re great, you don’t need to worry about that when you’re inside.
The suspending world is nicely sequential
You have probably noticed that suspending functions don’t have special return types. They are really declared just like usual functions. We don’t need any wrapper like Java’s Future
or JavaScript's Promise
. This confirms that suspending functions are not asynchronous themselves (at least from the point of view of the caller), unlike JavaScript’s async
functions, which return promises.
From inside a suspending function, we can reason sequentially about function calls
This is what makes asynchronous stuff easy to reason about in Kotlin. Inside a suspending function, calls to other suspending functions behave like normal function calls: we need to wait for the execution of the called function before getting the return value and executing the rest of the code.
That is what will allow us to write complex asynchronous code in a simple way later on.
Bridging the normal world and the suspending world
Calling a suspending function from a “normal” function directly cannot compile. The usual explanation is “because only coroutines can be suspended”, and from there we conclude that we need to somehow create a coroutine from which to run our suspending function. Great. But why?
Conceptually, suspending functions sort of announce from their declaration that they may “take some time to execute”. If you’re not a suspending function yourself, this forces you to do one of 2 things explicitly:
- actually block the thread while you’re waiting (like a normal, synchronous, function call)
- start something asynchronous to do it for you, and return immediately (which can be done in various possible ways)
You can see the creation of a coroutine as a way to express your choice, which must be explicit (and this is great!). This is done by using functions called coroutine builders.
Coroutine builders
Coroutine builders are simple functions that create a new coroutine to run a given suspending function. They can be called from normal non-suspending functions because they are not suspending themselves, and thus act as a bridge between the normal and the suspending world.
The Kotlinx Coroutines library contains multiple coroutine builders to do various things. We will see a few in the following subsections.
Block the current thread with “runBlocking”
The simplest way to deal with a suspending function from a normal function is to just block the current thread and wait. The coroutine builder to block the current thread is called runBlocking
:
In the context of runBlocking
, the given suspending function and its children in the call hierarchy will effectively block the current thread until it finishes executing.
As you can see from its signature, the function passed to runBlocking
is a suspending function, even though runBlocking
itself is not suspending (it is thread-blocking):
fun <T> runBlocking(
...,
block: suspend CoroutineScope.() -> T
): T {
...
}
This is often used from the main()
function to give a sort of top-level coroutine from which to work, and keep the JVM alive while doing so (we will see that in the section about structured concurrency).
Fire-and-forget with “launch”
Usually, the point of coroutines is not to block the thread, but rather start an asynchronous task. The coroutine builder called launch
allows to start a coroutine in background and keep working in the meantime.
From the Kotlin documentation, we have this example:
The comments should speak for themselves. This will print “Hello,” immediately, and add “World!” after a second.
Note that, for the purpose of the example, we need to somehow block the main function anyway in order to see what happens with launch
. That’s why they are re-using runBlocking
here, just to keep the JVM alive. (We could have used Thread.sleep()
but that wouldn’t be very Kotlin-esque now, would it?)
Don’t worry about this GlobalScope
object, I’ll get to it in just a minute.
Get a result asynchronously with “async”
Here is another coroutine builder called async
which allows to perform an asynchronous operation returning a value:
In order to get the result of the deferred value, async
returns a convenient Deferred
object, which is the equivalent of Future
or Promise
. We can call await
on this deferred value in order to wait and get the result.
await
is not a normal blocking function, it is a suspending function. This means we can’t just call it from main()
. We need to somehow actually block the main function in order to wait for the result, so we use runBlocking
here to wrap the call to await
.
The sharp eyes may have noticed the GlobalScope
again here, so I’m afraid I have to talk about it now. Coroutine scopes are the tool that allows us to create a hierarchy of coroutines. This is what the Kotlin team calls structured concurrency.
Structured concurrency
If you have followed the few examples above, you may have noticed that we needed to go through the classic “block and wait for my coroutines to finish” pattern.
In Java, this is usually obtained by keeping references to threads and calling join
on all of them in order to block the main thread while waiting for all the others. We could do a similar thing with Kotlin coroutines, but this is not idiomatic at all.
In Kotlin, coroutines can be created in a hierarchy, which allows a parent coroutine to automatically manage the life cycle of its child coroutines for you. It can for instance wait for its children to complete, or cancel all its children if an exception occurs in one of them.
Creating a hierarchy of coroutines
Except for runBlocking
, which should not be called from a coroutine, all coroutine builders are declared as extensions of the CoroutineScope
class, to encourage people to structure their coroutines:
fun <T> runBlocking(...): T {...}
fun <T> CoroutineScope.async(...): Deferred<T> {...}
fun <T> CoroutineScope.launch(...): Job {...}
fun <E> CoroutineScope.produce(...): ReceiveChannel<E> {...}
...
In order to create a coroutine, you either need to call these builders on the GlobalScope
(creating a top-level coroutine) or from an already existing coroutine scope (creating a child coroutine of that scope). In fact, if you write a function that creates coroutines, you should declare it as an extension of the CoroutineScope
class too. This is a convention that also happens to allow you to call coroutine builders easily, because a CoroutineScope
is available to you as this
.
If you take a look at coroutine builders’ signatures, you may notice that the suspending function they take as a parameter is also defined as an extension function of the CoroutineScope
class:
fun <T> CoroutineScope.async(
...
block: suspend CoroutineScope.() -> T
): Deferred<T> {
...
}
This means we can call other coroutine builders inside of that function without specifying any receiver, and the implicit receiver will be the child scope of the current coroutine, making it act as a parent. Easy!
Here is how we should structure the previous examples in a more idiomatic way:
Note that we don’t need GlobalScope
anymore, because a scope is provided by the wrapping runBlocking
call. Neither do we need extra delays here to wait for child coroutines to finish. The runBlocking
will wait for all its children to finish before finishing its own execution, and so the main thread will stay blocked as well, by definition of runBlocking
.
The coroutineScope builder
You may have noticed that the use of runBlocking
is discouraged from inside coroutines. This is because the Kotlin team wants to avoid thread-blocking functions inside coroutines, and uses suspending operations instead. The suspending equivalent of runBlocking
is the coroutineScope
builder.
coroutineScope
simply suspends the current coroutine until all child coroutines have finished their execution. Here is the example directly taken from the Kotlin documentation:
Beyond the basics
The basic building blocks I explained here are really not the greatest aspects of the coroutines concept in Kotlin. We can make use of coroutines to express concurrent stuff really nicely by using channels, flows, producers, actors etc. But I believe we first need to understand these building blocks before starting building higher abstractions on top of them.
We also haven't dug under the surface of suspending functions, and how they abstract away the continuation passing style, and how they are implemented using state machines (watch Deep Dive into Coroutines if you're interested).
There is a lot to say about coroutines, and this article barely scratches the surface of course, but I hope this post helped you understand coroutines and suspending functions better.
Please let me know if it was helpful to you, if you would like to know more about a particular aspect. Don’t hesitate to point out mistakes if you see any.
Very helpful resources
If you have some time to spare, I really recommend watching the Kotlin conf videos about coroutines by Roman Elizarov.
Coroutines in practice
In this talk, Roman does a very quick recap about coroutines, and then explains how to use them in a very nice way, using channels, actors etc.
Kotlin conf 2017
There are still helpful videos from the 2017 Kotlin conf:
- Introduction to Coroutines: this is basically the content of my article but with more accurate and better explanations.
- Deep Dive into Coroutines: explains the details about how suspending functions and coroutines are actually implemented. Very enlightening.