Chapter 23: Coroutines – Basics (Kotlin’s #1 Superpower)
Coroutines – Basics (Kotlin’s #1 Superpower) — this is the chapter that makes Kotlin feel like a superhero language! ☕🚀
Coroutines are Kotlin’s way to handle concurrency (doing multiple things at once) in a simple, safe, and efficient manner. They’re like lightweight threads — but without the complexity, memory overhead, or blocking issues of traditional threads. In 2026, coroutines are everywhere — Android apps, backend servers (Ktor, Spring WebFlux), games, data processing… you name it!
If threads are like hiring a team of heavy-duty workers (expensive, hard to manage), coroutines are like super-efficient multitasking robots that can switch tasks instantly without wasting resources.
We’re going to cover:
- Suspending functions (suspend)
- runBlocking
- launch
- async / await
- Coroutine builders & scopes
I’ll explain everything very slowly, like we’re sitting together in a quiet Bandra café — with real-life analogies, complete runnable examples (copy-paste into IntelliJ with Kotlin 1.9+), step-by-step breakdowns, tables, common mistakes with fixes, and fun facts so everything sticks perfectly.
Important setup:
- Add this to your build.gradle.kts (or build.gradle):
Kotlin0123456implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") // Latest in 2026
- Or in IntelliJ: File → New → Project → Kotlin → Multiplatform → Add coroutines dependency.
Let’s start!
1. Suspending Functions (suspend) – The Foundation of Coroutines
A suspending function is a function that can pause its execution at certain points (suspension points) without blocking the thread — and resume later.
Key points:
- Marked with suspend keyword
- Can call other suspend functions (like delay())
- Does not block the thread — frees it for other work
- Can only be called from another suspend function or from a coroutine builder (like launch, async)
Real-life analogy: You’re cooking rice (long task).
- Normal function → stand there waiting (block the kitchen)
- Suspend function → set timer, go do other chores (free the kitchen), come back when done.
Example 1 – Basic suspend function
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import kotlinx.coroutines.* suspend fun longTask() { println("Starting long task...") delay(2000) // Suspend for 2 seconds – non-blocking! println("Long task finished!") } fun main() { // ERROR! Cannot call suspend from non-suspend // longTask() // Compile error // Use coroutine builder to call it (more on this later) runBlocking { longTask() // OK inside coroutine } } |
Output (after 2 seconds):
|
0 1 2 3 4 5 6 7 |
Starting long task... Long task finished! |
Step-by-step:
- suspend fun → marks it as suspendable
- delay(2000) → suspension point → pauses coroutine, not thread
- Cannot call from normal main() — must use a coroutine context (like runBlocking)
Fun fact: delay() is like Thread.sleep() but non-blocking — the thread is free to do other work!
2. runBlocking – Bridge Between Blocking & Non-Blocking Worlds
runBlocking is a coroutine builder that:
- Blocks the current thread until all coroutines inside finish
- Used to call suspend functions from non-suspend code (like main())
Use it for:
- Main function in console apps
- Unit tests
- Bridge to legacy blocking code
Do NOT use in:
- Android UI (blocks main thread → ANR)
- Servers (blocks worker threads)
Example 2 – runBlocking with multiple suspend calls
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
suspend fun task1() { delay(1000) println("Task 1 done") } suspend fun task2() { delay(2000) println("Task 2 done") } fun main() = runBlocking { // runBlocking makes main() a coroutine scope task1() task2() println("All tasks finished!") } |
Output (after 3 seconds total):
|
0 1 2 3 4 5 6 7 8 |
Task 1 done Task 2 done All tasks finished! |
Step-by-step:
- runBlocking { … } → creates a blocking coroutine on current thread
- Inside the block → you can call suspend functions
- It waits (blocks) until everything inside finishes
3. launch – Fire-and-Forget Coroutines
launch is a coroutine builder that:
- Launches a new coroutine concurrently
- Returns a Job (handle to control/cancel it)
- Does not return a result — use for side-effects (fire-and-forget)
Real-life analogy: You’re the boss — launch is like telling your assistant: “Go do this task in background — I’ll continue my work.”
Example 3 – launch concurrent coroutines
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
fun main() = runBlocking { val job1 = launch { delay(1000) println("Job 1 finished") } val job2 = launch { delay(2000) println("Job 2 finished") } println("Main is waiting...") job1.join() // Wait for job1 job2.join() // Wait for job2 println("All jobs done!") } |
Output (after 2 seconds total):
|
0 1 2 3 4 5 6 7 8 9 |
Main is waiting... Job 1 finished Job 2 finished All jobs done! |
Step-by-step:
- launch { … } → starts a new coroutine in parallel
- Returns Job → use join() to wait, cancel() to stop
- Inside runBlocking → main thread waits for joins
4. async / await – Coroutines with Results
async is like launch but:
- Returns a Deferred<T> (promise/future with result)
- Use await() to get the result (suspends until ready)
Real-life analogy: You ask your assistant: “Go buy groceries and bring back the receipt” (async) → you wait for the receipt (await) before paying bills.
Example 4 – async / await concurrent tasks
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
suspend fun fetchUser(id: Int): String { delay(1500) // Simulate network call return "User $id" } suspend fun fetchOrder(id: Int): String { delay(2000) // Simulate API call return "Order $id" } fun main() = runBlocking { val userDeferred = async { fetchUser(1) } val orderDeferred = async { fetchOrder(1) } println("Waiting for data...") val user = userDeferred.await() // Suspend until ready val order = orderDeferred.await() println("User: $user, Order: $order") } |
Output (after 2 seconds total — concurrent!):
|
0 1 2 3 4 5 6 7 |
Waiting for data... User: User 1, Order: Order 1 |
Step-by-step:
- async { … } → starts concurrent coroutine, returns Deferred<T>
- await() → suspends until result ready → returns T
- Runs in parallel → total time is max of delays (2s, not 3.5s)
Common pattern – structured concurrency: All launch / async inside a scope → parent waits for children.
5. Coroutine Builders & Scopes – The Structure
Coroutine builders = functions that start coroutines (launch, async, runBlocking)
Coroutine scopes = contexts that manage lifecycle of coroutines (wait for children, cancel, handle exceptions).
| Builder | Returns | Blocks? | Purpose |
|---|---|---|---|
| runBlocking | Lambda result | Yes | Bridge blocking code to coroutines |
| launch | Job | No | Fire-and-forget side effects |
| async | Deferred<T> | No | Compute value concurrently |
| coroutineScope | Lambda result | No (suspends) | Structured scope inside suspend function |
GlobalScope (avoid in 2026!):
- GlobalScope.launch { … } → runs globally — not structured, can leak
- Best practice: Always use structured scopes (coroutineScope, viewModelScope, etc.)
Example 5 – coroutineScope for structured concurrency
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
suspend fun doTasks() = coroutineScope { // New scope launch { delay(1000); println("Task 1 done") } launch { delay(2000); println("Task 2 done") } println("Waiting inside scope...") } // Suspends until both launches finish fun main() = runBlocking { doTasks() println("All tasks completed!") } |
Output:
|
0 1 2 3 4 5 6 7 8 9 |
Waiting inside scope... Task 1 done Task 2 done All tasks completed! |
Step-by-step:
- coroutineScope { … } → creates a child scope
- All launches inside are children — scope waits for them
- If one child crashes → all siblings cancel + exception propagates
Quick Recap Table (Your Cheat Sheet)
| Concept | Key Points / Example | Benefit |
|---|---|---|
| suspend | suspend fun longTask() { delay(1000) } | Non-blocking pause |
| runBlocking | runBlocking { longTask() } | Block thread for coroutines |
| launch | launch { delay(1000) } → Job | Concurrent side-effect |
| async | async { delay(1000) } → Deferred<T> | Concurrent value computation |
| await | deferred.await() → T | Get async result |
| coroutineScope | coroutineScope { launch { … } } | Structured concurrency – wait for children |
Common Newbie Mistakes & Fixes
| Mistake | Problem | Fix |
|---|---|---|
| Calling suspend from non-suspend | Compile error | Use runBlocking or another builder |
| Not using await on async | Deferred<T> not resolved | Always call .await() |
| Using GlobalScope | Leaks, unstructured | Use coroutineScope or structured scopes |
| Forgetting to import kotlinx.coroutines | Cannot find delay, launch | Add dependency & import kotlinx.coroutines.* |
| Blocking in UI coroutines | UI freeze | Use non-blocking suspend (delay, not Thread.sleep) |
Homework for You (Let’s Make It Fun!)
- Basic Create a suspend function suspend fun helloDelay(name: String) → delay 1s → print “नमस्ते $name!”. Call from runBlocking.
- Medium Use launch to run 3 concurrent tasks that print after different delays.
- Advanced Use async to fetch two values concurrently → await both → print their sum.
- Fun Simulate a race: 2 coroutines “running” with delays → first to finish wins!
- Challenge Fix this code:
Kotlin0123456789fun main() {async { delay(1000) } // Not running!println("Done")}
You’ve just unlocked Kotlin’s #1 superpower — coroutines! Now you can write concurrent code that’s simple, safe, and efficient.
