Coroutines Intro
Reviewed & published by Brayan K
Kotlin is a modern, concise language whose coroutines let you write asynchronous, non-blocking code that reads like ordinary sequential code — no callback pyramids required.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll write suspend functions, launch coroutines, and run work concurrently with async/await.
What You'll Learn in This Lesson
- What a coroutine is and why it's lightweight
- The suspend keyword and suspension points
- Non-blocking delay vs Thread.sleep
- runBlocking to bridge into coroutines
- launch for fire-and-forget work
- async/await for concurrent results
1️⃣ suspend Functions and runBlocking
A suspend function can pause at suspension points (like delay) without blocking its thread. You can only call one from another suspend function or a coroutine builder. runBlocking bridges ordinary code into the coroutine world — common in main and tests.
import kotlinx.coroutines.*
// 'suspend' marks a function that can PAUSE without blocking a thread.
suspend fun fetchUser(): String {
delay(1000) // suspends for 1s (non-blocking, not Thread.sleep)
return "Ada"
}
// runBlocking bridges normal code and the coroutine world (used in main/tests).
fun main() = runBlocking {
println("Fetching...")
val user = fetchUser() // suspends here, frees the thread to do other work
println("Got user: $user")
println("Done")
}The code reads top-to-bottom like normal sequential logic, yet the delay doesn't freeze the thread — that's the magic of coroutines.
2️⃣ launch, async, and Concurrency
launch starts a fire-and-forget coroutine and returns a Job. async starts one that produces a result as a Deferred, which you read with await(). Two async calls run concurrently.
import kotlinx.coroutines.*
suspend fun loadProfile(): String { delay(1000); return "profile" }
suspend fun loadSettings(): String { delay(1000); return "settings" }
fun main() = runBlocking {
// launch starts a coroutine that runs concurrently (fire-and-forget).
val job = launch {
delay(500)
println("Background task finished")
}
// async starts a coroutine that returns a Deferred result.
// Two async calls run AT THE SAME TIME, so total wait ~1s, not 2s.
val profile = async { loadProfile() }
val settings = async { loadSettings() }
// await() suspends until each result is ready:
println("Loaded: ${profile.await()} + ${settings.await()}")
job.join() // wait for the launched job too
println("All done")
}Both loads each take a second, but because they ran concurrently the total wait is about one second, not two. That's how coroutines turn sequential waits into parallel ones.
Your turn. Replace the TODO, then run and compare.
// 🎯 YOUR TURN — replace each TODO then run main()
import kotlinx.coroutines.*
// 1) Mark this function so it can call delay().
TODO fun greetAfterDelay(): String { // 👉 replace TODO with 'suspend'
delay(500)
return "Hello after delay"
}
fun main() = runBlocking {
println("Start")
println(greetAfterDelay()) // suspends, then returns
println("End")
// ✅ Expected:
// Start
// Hello after delay
// End
}Mini-Challenge: Concurrent Loads
Run two suspend functions concurrently with async and combine their results.
// 🎯 MINI-CHALLENGE: load two things concurrently.
// Save as Concurrent.kt and run main().
//
// 1. Write suspend fun price(): Int { delay(300); return 100 }.
// 2. Write suspend fun tax(): Int { delay(300); return 20 }.
// 3. In runBlocking, start BOTH with async, then await() each and
// print "Total: <price + tax>".
// (Because they run concurrently, this takes ~300ms, not 600ms.)
//
// ✅ Example output:
// Total: 120
import kotlinx.coroutines.*
fun main() = runBlocking {
// your code here
}Common Errors (and the fix)
- "Suspend function can only be called from a coroutine" — Call it inside runBlocking, launch, or another suspend function.
- Using Thread.sleep — That blocks the thread. Use delay inside coroutines instead.
- Awaiting sequentially by mistake — Calling async { ... }.await() back-to-back is sequential. Start both first, then await, for concurrency.
- Forgetting to import — Add import kotlinx.coroutines.* and the coroutines dependency.
Pro Tips
- 💡 Structured concurrency: child coroutines launched in a scope are cancelled together — leaks are prevented by design.
- 💡 coroutineScope waits for all its children, a cleaner alternative to manual join().
- 💡 Dispatchers choose the thread pool: Dispatchers.IO for network/disk, Default for CPU work.
📋 Quick Reference — Coroutines
| Concept | Kotlin Syntax |
|---|---|
| Suspend function | suspend fun load() |
| Non-blocking wait | delay(1000) |
| Bridge into coroutines | runBlocking { ... } |
| Fire-and-forget | launch { ... } |
| Concurrent result | async { ... } |
| Get the result | deferred.await() |
🎉 Lesson Complete!
- ✅ Coroutines are lightweight, suspendable units of work
- ✅ suspend marks functions that can pause without blocking
- ✅ delay is non-blocking; Thread.sleep is not
- ✅ launch is fire-and-forget; async/await return results
- ✅ Concurrent async calls run in parallel
Practice quiz
What does the 'suspend' keyword mark a function as?
- A function that can pause at suspension points without blocking its thread
- A function that always runs on a new thread
- A function that cannot be cancelled
- A function that blocks the main thread
Answer: A function that can pause at suspension points without blocking its thread. suspend marks a function that may pause and resume later without blocking the underlying thread.
Where can a suspend function be called from?
- Any ordinary function
- Only the main function
- Another suspend function or a coroutine builder like launch/async/runBlocking
- Only from inside a class
Answer: Another suspend function or a coroutine builder like launch/async/runBlocking. Suspend functions can only be called from another suspend function or a coroutine builder.
How does delay() differ from Thread.sleep()?
- delay() is slower than Thread.sleep()
- They are identical
- delay() blocks two threads at once
- delay() suspends the coroutine and frees the thread; Thread.sleep blocks the thread
Answer: delay() suspends the coroutine and frees the thread; Thread.sleep blocks the thread. delay suspends only the current coroutine, releasing the thread; Thread.sleep blocks the whole thread.
What does the launch coroutine builder return?
- A Job
- A Deferred
- A String
- Nothing at all
Answer: A Job. launch starts fire-and-forget work and returns a Job you can join or cancel.
What does async return?
- A Job
- A Deferred that you read with await()
- A plain value immediately
- A Thread
Answer: A Deferred that you read with await(). async returns a Deferred<T>; call await() to get the result.
What is runBlocking typically used for?
- Cancelling all coroutines
- Replacing the JVM thread pool
- Bridging ordinary code into the coroutine world, e.g. in main and tests
- Creating new threads only
Answer: Bridging ordinary code into the coroutine world, e.g. in main and tests. runBlocking bridges normal blocking code and the coroutine world.
How do you get the result out of a Deferred?
- By calling .join()
- By calling .await()
- By calling .close()
- By reading .value directly
Answer: By calling .await(). await() suspends until the Deferred's result is ready and returns it.
Starting two async blocks and then awaiting both lets the work run...
- Sequentially, one after the other
- On the UI thread only
- Never, it deadlocks
- Concurrently, so total wait is about one delay not two
Answer: Concurrently, so total wait is about one delay not two. Both async coroutines run concurrently, so two 1s tasks finish in about 1s total.
What does structured concurrency guarantee about child coroutines in a scope?
- They are cancelled together with the scope
- They run on separate processes
- They never get cancelled
- They ignore their parent scope
Answer: They are cancelled together with the scope. Children launched in a scope are cancelled together, preventing leaks by design.
Which Dispatcher is intended for network and disk work?
- Dispatchers.Main
- Dispatchers.Default
- Dispatchers.IO
- Dispatchers.Unconfined only
Answer: Dispatchers.IO. Dispatchers.IO is for blocking I/O like network and disk; Default is for CPU work.
Continue this course
- Previous: Scope Functions (let, run, apply, also, with)
- Next: Exception Handling — Handle errors gracefully with try/catch and custom exceptions
- Quick reference: Kotlin cheat sheet › Coroutines
Frequently asked questions
What is a coroutine, in plain terms?
A coroutine is a lightweight unit of work that can suspend (pause) and later resume without blocking the underlying thread. Think of it as a function you can pause partway through — say, while waiting for a network response — and pick up again later, all while the thread is free to run other coroutines. Because they're cheap, a single thread can host thousands of coroutines, which is why they're great for highly concurrent apps and Android UIs that must stay responsive.
What does the 'suspend' keyword mean?
suspend marks a function that may pause execution at certain points (its suspension points, like delay or a network call) and resume later. A suspend function can only be called from another suspend function or from inside a coroutine builder such as launch, async, or runBlocking — never from ordinary code directly. This rule makes the asynchronous boundaries visible in your code, so you always know which functions might pause.
How is delay different from Thread.sleep?
Thread.sleep blocks the whole thread — it sits there doing nothing, wasting that thread. delay suspends only the current coroutine and releases the thread so other coroutines can run during the wait. That's the essence of non-blocking concurrency: instead of freezing a thread, the coroutine steps aside and is resumed when the time is up. Use delay inside coroutines; never Thread.sleep.
When do I use launch versus async?
Use launch for fire-and-forget work where you don't need a return value — it returns a Job you can join or cancel. Use async when you want a result back: it returns a Deferred<T>, and you call await() to get the value (suspending until it's ready). A common pattern is to start several async calls so they run concurrently, then await each — turning sequential waits into parallel ones, which is exactly what the concurrent example demonstrates.