suspend functions and the basics
The building block of coroutines is the suspend function — a function that can pause partway
through, without blocking its thread, and resume later. This lesson is what suspend means, how you
run one, and the two ways to run several at once. Every example here was executed against real
coroutines.
The suspend keyword
Mark a function suspend and it gains the ability to pause:
suspend fun fetchUser(id: Int): String {
delay(1000) // pause for 1 second WITHOUT blocking the thread
return "User $id"
}
delay(1000) looks like "sleep for a second", but it is fundamentally different from
Thread.sleep(1000): delay suspends the coroutine (releasing the thread for other work) and
resumes it a second later, while Thread.sleep would block the thread (holding it uselessly).
delay is itself a suspend function — and here is the core rule:
A suspend function can only be called from another suspend function, or from a coroutine. You
cannot call fetchUser from an ordinary function. This is the compiler enforcing a real constraint:
suspending only makes sense inside a coroutine that knows how to pause and resume. Try it and the
compiler stops you:
fun main() {
fetchUser(1) // error: suspend function 'fetchUser' should be called only from a coroutine or another suspend function
}
Starting a coroutine: runBlocking
To call a suspend function you need a coroutine, and something has to start the first one.
runBlocking is the bridge from ordinary code into the coroutine world — it starts a coroutine and
blocks until it finishes:
import kotlinx.coroutines.*
fun main() = runBlocking { // this block IS a coroutine
println("Fetching...")
val user = fetchUser(1) // now we can call suspend functions
println(user)
}
Fetching...
User 1
runBlocking is mostly for main functions, tests, and examples — it is the one place where
blocking is acceptable, because you want main to wait for the work. In real Android code you do
not use runBlocking on the main thread (that would block the UI); you launch coroutines in a
scope, which the next lesson covers. For learning and for main, runBlocking is exactly right.
Sequential by default
Inside a coroutine, suspend calls run in order, top to bottom — just like ordinary code:
suspend fun fetchUser(id: Int): String { delay(1000); return "User $id" }
suspend fun fetchOrders(user: String): Int { delay(1000); return 3 }
fun main() = runBlocking {
val start = System.currentTimeMillis()
val user = fetchUser(1) // 1 second
val orders = fetchOrders(user) // then another second
println("$user has $orders orders")
println("Took ${System.currentTimeMillis() - start} ms") // ~2000 ms
}
This takes about two seconds, because the two one-second calls run one after the other. That is correct when the second call depends on the first (you need the user before you can fetch their orders). The code reads sequentially and runs sequentially — exactly the readability coroutines promise.
Running in parallel: async and await
But what if the two calls are independent — you want both, and neither needs the other? Running
them sequentially wastes time. async starts a coroutine that computes a value, returning a
Deferred (a promise of a result); await() waits for that result:
fun main() = runBlocking {
val start = System.currentTimeMillis()
val userDeferred = async { fetchUser(1) } // starts now, runs concurrently
val ordersDeferred = async { fetchCount() } // also starts now
val user = userDeferred.await() // wait for the first
val count = ordersDeferred.await() // wait for the second
println("$user, $count items")
println("Took ${System.currentTimeMillis() - start} ms") // ~1000 ms, not 2000!
}
This takes about one second, not two — both async blocks started immediately and ran
concurrently, so the two one-second waits overlapped. async for concurrent, independent work;
plain sequential calls for dependent work. Choosing correctly is a real skill: use async only
when the tasks genuinely do not depend on each other, and you will halve (or better) your waiting
time.
launch versus async
Two coroutine builders, for two purposes:
launch— start a coroutine that does something but returns no result. It gives you aJob(a handle to cancel or wait for it withjoin()). Use it for fire-and-forget work: log this, save that, update the UI.async— start a coroutine that computes a value. It gives you aDeferred<T>, whoseawait()returns the result. Use it when you need the value back.
val job = launch { saveToDatabase(order) } // no result; job.join() waits, job.cancel() stops
val deferred = async { computeTotal(order) } // a result; deferred.await() gets it
The rule of thumb: launch when you want the side effect, async when you want the value. Both
start a coroutine; the difference is whether it hands you something back.
Check your work
What suspend gives a function. The ability to pause without blocking its thread, and resume
later.
How delay differs from Thread.sleep. delay suspends (releasing the thread); Thread.sleep
blocks (holding the thread uselessly).
The rule about calling a suspend function. Only from another suspend function or from a coroutine.
What runBlocking does, and where to use it. Bridges ordinary code into a coroutine and blocks
until done — for main, tests, and examples, not the Android main thread.
How suspend calls run by default. Sequentially, in order — right for dependent work.
What async/await do. Start concurrent work returning a Deferred, whose await() gives the
result — for independent work, overlapping the waits.
When to use async versus sequential calls. async for independent tasks; sequential when one
depends on the previous.
launch versus async. launch for a side effect (returns a Job); async for a value
(returns a Deferred).
Practice
- Write a
suspend funthatdelays and returns a value. Try to call it from an ordinaryfunand read the compiler error. - Call it correctly from
runBlockingand print the result. - Write two dependent suspend calls in sequence and time the total (expect the sum of the delays).
- Make them independent and run them with
async/await. Time it and confirm it is now roughly the longer of the two, not the sum. - Use
launchto run a fire-and-forget task andjoin()it. Then useasyncfor one that returns a value andawait()it. Note which gives you a result. - Replace a
delaywithThread.sleepinside a coroutine and reason about why that is wrong (it blocks the thread the coroutine was sharing). - Take three imagined async calls, decide which are dependent and which independent, and write the fastest correct combination.
Official documentation
- Kotlin — Coroutines basics —
runBlocking,launch,delay, and suspend functions. - Kotlin — Composing suspending functions — Sequential versus
asyncconcurrent. - Kotlin — Suspend functions — The
suspendkeyword and the call rule.
Next: scopes and dispatchers — where coroutines actually run.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship