“Grokking” Kotlin Coroutines: Why suspend exists
A practical mental model for understanding async work, threads, callbacks, Rx, and why Kotlin suspend exists.
“Grokking” Kotlin Coroutines: Why suspend exists

A long time ago, in a far, far galaxy… Oh, wait, this is the wrong title. Let’s try again.
What are concurrency, async, and multithreading to you? Is it something that you struggle with? Is it something that gives you goosebumps when you see a Jira task with an appropriate description?
Don’t worry! I got you! For me, this was a dark forest many years ago. There was no help, no proper descriptions, no docs(LOL), no info(yeah, StackOverflow was working, but cmon).
In this set of articles, we will cover such topics as:
- What is actual async work, and how does it differ from concurrency and multithreading?
- How to work with different async requests in Android and what will happen if you make it wrong?
- How to improve your current async solutions — the Coroutines way.
This series will not try to replace the official documentation. Instead, it will try to give you a mental model that makes the documentation easier to understand.
So let’s begin our journey…

What is actual async work, and how does it differ from concurrency and multithreading?
Concurrency is about dealing with multiple tasks at the same time. Parallelism is about running multiple tasks at the same physical time. Multithreading is one way to achieve concurrency using OS threads. Asynchronous programming is about not waiting/blocking while work is in progress.
Imagine a screen that loads user data, posts, and settings. With callbacks, you quickly get nesting. With Rx, you get a stream-based model. With coroutines, you can keep the code sequential while still avoiding blocking the main thread.
In the book definitions of async, concurrency, and multithreading, we can see this:

Yeah, book page here is a Wiki
But what is actually async in Kotlin, and what is it doing?
Threads

Let’s go deep into the short history of async programming. Way back before, there was only Threads…
Developers used Threads as a regular approach of programming, but they have their own complexities and compromises:
- Threads aren’t cheap. Threads require costly context switches.
- Threads aren’t infinite. The number of threads that can be launched is limited by the underlying operating system. In backend applications, this could cause a major bottleneck.
- Threads aren’t always available. Some platforms, such as JavaScript, do not even support threads.
- Threads aren’t easy. Debugging threads and avoiding race conditions are common problems we suffer in multi-threaded programming.
Callbacks

After that, Callbacks came into play… But this approach also has its own problems. What does it give us?
- Callbacks are easy to create and use
- Functions can take callbacks as parameters. Callback will return some value from a thread or another callback when some work is done.
But the downsides are pretty significant:
- Difficulty of nested callbacks. Usually, a function that is used as a callback often ends up needing its own callback. This leads to a series of nested callbacks, which lead to incomprehensible code. The pattern is often referred to as callback hell, or the pyramid of doom, due to the triangular shape that indentations from these deeply nested callbacks create.
- Error handling is complicated. The nesting model makes error handling and propagation of these somewhat more complicated.
Futures and promises

Threads are one possible execution mechanism underneath them, but a future/promise is primarily an abstraction for a value that will exist later. So we can say that futures and promises are promising that we will return a Promise object, and then we can operate on it.
fun postItem(item: Item) {
preparePostAsync()
.thenCompose { token ->
submitPostAsync(token, item)
}
.thenAccept { post ->
processPost(post)
}
}
fun preparePostAsync(): Promise<Token> {
// makes request and returns a promise that is completed later
return promise
}
To use that approach to its full potential, we should change our program for that:
- Different programming model. Similar to callbacks, the programming model moves away from a top-down imperative approach to a compositional model with chained calls. Traditional program structures such as loops, exception handling, etc., are no longer valid in this model.
- Different APIs. Usually, there’s a need to learn a completely new API, such as
thenComposeorthenAccept, which can also vary across platforms. - Specific return type. The return type moves away from the actual data that we need and instead returns a new type
Promisewhich has to be introspected. - Error handling can be complicated. The propagation and chaining of errors aren’t always straightforward.
Reactive extensions

Oh mate, this was a shiny start at the moment when it came… Reactive extensions, or Rx were mainstream. It came to Java first with the name RxJava. For many Java and Android developers, RxJava became the version that made reactive programming feel mainstream.
Rx is built around observable streams: instead of treating data as a single value that appears once, it treats data as a sequence of values that may arrive over time. A stream can emit zero, one, many, or even an unbounded number of values, and consumers can subscribe to react to those emissions.
In practice, Rx extends the Observer pattern with a rich set of operators for transforming, combining, filtering, retrying, and scheduling asynchronous data flows.
Its model is similar to futures and promises, but the key difference is cardinality:
- A
Futurerepresents one eventual result or failure. - An Rx stream represents a sequence of results over time, followed by either completion or an error.
So, while a future answers “what value will be available later?”, Rx answers “what values may arrive over time, and how should they be processed?”
And by tradition, pros and cons: Pros
- Handles streams of data over time: zero, one, many, or infinite values.
- Provides powerful operators for filtering, combining, retrying, debouncing, and transforming data.
- Works well for reactive UI, database updates, sockets, Bluetooth, and user events.
Cons
- Higher learning curve and more complex code for simple cases.
- Debugging chains and handling errors can be difficult.
- Requires careful subscription disposal and thread management to avoid leaks and unexpected behavior.
And all of these approaches lead us to Coroutines…
Coroutines

So, what are coroutines and how do they work?
Coroutines are a way to write asynchronous, non-blocking code in a sequential style. They are often called lightweight, but they are not lightweight threads. A coroutine is not an actual OS thread.
Threads are expensive because creating and maintaining them requires memory, including a dedicated stack, and the operating system must schedule them. Switching between threads also has a cost because the system needs to save and restore their execution state.
Coroutines reduce this overhead by avoiding the need for one thread per task. When a coroutine reaches a suspension point, such as await() or delay(), it pauses without blocking its thread. The thread is then free to execute other work.
Later, when the result becomes available, the coroutine resumes from the same point. This allows thousands of coroutines to run efficiently on a small number of threads.
This is all theory… But what about practice? Does this have positive sides?
YES! YES! YES! Coroutines let us write asynchronous, non-blocking code in a sequential style!
Fantastic, right? The suspend keyword does not magically move work to another thread.
It only allows the function to suspend and resume later without blocking the current thread, if the suspending operation is implemented that way.
Check it out:
fun postItem(item: Item) {
launch {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
}
suspend fun preparePost(): Token {
// makes a request and suspends the coroutine
return suspendCoroutine { /* ... */ }
}
preparePost is a suspending function, so it can pause and resume later without blocking the main thread. While it is waiting for data, the thread can continue handling other work.
The function still looks like normal sequential code: it has a regular return type and uses familiar control flow, such as loops and try/catch. The only difference is the suspend modifier.
Coroutines are supported across Kotlin platforms. Most coroutine features, including launch, async, and await, come from libraries rather than special language syntax.
Conclusion

At this point, the most important idea is not the syntax. It is the mental model.
A coroutine is not a magic background thread. A suspend function is not automatically asynchronous. And launch is not just a nicer way to start random work somewhere.
Coroutines are Kotlin’s way to structure asynchronous work so that it still looks and feels like normal code.
That is the real win.
We keep the readability of sequential code, but we also get tools for cancellation, lifecycle, error handling, and composition.
In the next part, we will stop talking about coroutines in theory and start looking at what actually happens when we launch coroutine work in Android:
Who owns it? Where does it run? What cancels it? And what breaks when we choose the wrong scope?
메타데이터
- post_id
- a4f81160c5a2
- slug
- grokking-kotlin-coroutines-why-suspend-exists-a4f81160c5a2
- url
- https://medium.com/@yevheniikrykun/grokking-kotlin-coroutines-why-suspend-exists-a4f81160c5a2
- canonical_url
- https://medium.com/@yevheniikrykun/grokking-kotlin-coroutines-why-suspend-exists-a4f81160c5a2
- author_url
- https://medium.com/@yevheniikrykun
- status
- ok
- fetched_at
- 2026-06-20 20:29:01