RizTech Academy logo
RizTech Academy
CoroutinesLesson 2 of 530 min

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 a Job (a handle to cancel or wait for it with join()). 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 a Deferred<T>, whose await() 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

  1. Write a suspend fun that delays and returns a value. Try to call it from an ordinary fun and read the compiler error.
  2. Call it correctly from runBlocking and print the result.
  3. Write two dependent suspend calls in sequence and time the total (expect the sum of the delays).
  4. 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.
  5. Use launch to run a fire-and-forget task and join() it. Then use async for one that returns a value and await() it. Note which gives you a result.
  6. Replace a delay with Thread.sleep inside a coroutine and reason about why that is wrong (it blocks the thread the coroutine was sharing).
  7. Take three imagined async calls, decide which are dependent and which independent, and write the fastest correct combination.

Official documentation

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