Why asynchronous code exists, with a real example
This module is, along with null safety, one of the two reasons Kotlin is a joy for Android. It is also where junior developers most reliably come unstuck in interviews and on the job — so it is worth going slowly and understanding the why before the how. This first lesson is the problem asynchronous code solves, and why coroutines are Kotlin's unusually good answer.
The problem: waiting
Programs spend a lot of time waiting — for a network request, a database query, a file to load, a user to tap. On a phone, these waits are long: a network call over patchy mobile data can take seconds. The question is what your program does while it waits.
The naive answer — just wait, doing nothing — is a disaster on a phone, because of one hard rule:
The main thread must never be blocked. In an Android app, one special thread — the main (or UI) thread — draws the screen and handles taps. If you make a two-second network call on that thread, the entire UI freezes for two seconds: nothing animates, taps do nothing, and if it lasts long enough Android shows the dreaded "Application Not Responding" dialog and the user force-closes your app. Blocking the main thread is the single most common way to make an app feel broken.
So the work that waits must happen somewhere else, off the main thread, and the result must come back to update the UI. That "do slow work elsewhere, come back when done" is asynchronous programming, and every app does a great deal of it.
The old answer: callbacks, and callback hell
Before coroutines, the standard tool was the callback — you start the slow work and hand it a function to call when it finishes:
// the old, painful way (pseudocode)
fetchUser(id) { user ->
fetchOrders(user.id) { orders ->
fetchOrderDetails(orders.first()) { details ->
updateUI(details) // finally!
}
}
}
Three sequential steps, each depending on the last, and the code marches off the right edge of the screen — nested callback inside callback inside callback. This is "callback hell", and it is genuinely awful: the logic reads inside-out, error handling has to be threaded through every level, and cancelling the whole thing partway is a nightmare. Asynchronous code written this way is where a lot of app bugs live.
Coroutines: asynchronous code that reads like ordinary code
Kotlin's coroutines let you write the same asynchronous logic as if it were sequential, top-to- bottom code:
// the coroutine way — reads like it runs in order, but does not block
suspend fun loadDetails(id: Int) {
val user = fetchUser(id) // waits, without blocking the thread
val orders = fetchOrders(user.id) // then this
val details = fetchOrderDetails(orders.first()) // then this
updateUI(details)
}
Read that: it looks exactly like ordinary sequential code — do this, then this, then this. But each
of those waiting calls suspends rather than blocks: the coroutine pauses, frees the thread to do
other work, and resumes where it left off when the result is ready. No nesting, no callbacks, error
handling with ordinary try/catch, and the whole thing reads in the order it happens. This is the
headline achievement of coroutines: the readability of sequential code with the efficiency of
asynchronous code.
Suspending is not blocking — the key idea
The distinction the whole module rests on:
- Blocking a thread means the thread sits there doing nothing, unavailable for other work, until the wait is over. A blocked main thread is a frozen UI.
- Suspending a coroutine means the coroutine pauses and releases the thread to run other work; when the awaited result arrives, the coroutine resumes (possibly on a different thread). The thread was never stuck.
This is why coroutines are so efficient: thousands of coroutines can share a handful of threads, because a suspended coroutine costs almost nothing — it is not holding a thread hostage. You can launch ten thousand coroutines on a phone; you could not launch ten thousand threads.
Why "lightweight" matters
A thread is a heavy operating-system resource — each one uses real memory and the OS must schedule it. A coroutine is lightweight — essentially a suspendable computation the Kotlin runtime manages, many to a thread. This is not a minor efficiency point; it changes what is possible. Doing a hundred network calls concurrently with threads is expensive and fragile; with coroutines it is routine. That efficiency, plus the sequential readability, plus structured concurrency (a later lesson, and the feature that makes coroutines safe as well as pleasant) is why coroutines have become the standard way to do asynchronous work in Kotlin and Android.
What is coming in this module
suspendfunctions — the building block: functions that can pause without blocking.- Scopes and dispatchers — where coroutines run, and how to move work off the main thread.
- Structured concurrency and cancellation — how coroutines are organised so they cannot leak, and how to stop them cleanly. This is the part that makes coroutines safe.
- Flow — a stream of values over time, for data that arrives in pieces.
By the end you will be able to write asynchronous code that is correct, safe, and reads like the sequential logic it implements — which is exactly what Android will ask of you constantly.
Check your work
The hard rule about the main thread. Never block it — a blocked main thread freezes the UI and triggers "Application Not Responding".
What asynchronous programming does. Runs slow, waiting work off the main thread and brings the result back.
What callback hell is. Nested callbacks for sequential async steps — logic reads inside-out, error handling is painful, cancellation is hard.
What coroutines achieve. Asynchronous code written as sequential, top-to-bottom code — readable and efficient.
Blocking versus suspending. Blocking holds a thread hostage doing nothing; suspending pauses the coroutine and releases the thread to do other work, resuming when the result is ready.
Why coroutines are efficient. A suspended coroutine holds no thread, so thousands share a few threads.
Why "lightweight" changes what is possible. Threads are heavy OS resources; coroutines are cheap, so massive concurrency becomes routine.
The four things this module covers. suspend functions, scopes/dispatchers, structured
concurrency/cancellation, and Flow.
Practice
- Explain, in two sentences, why a two-second network call on the main thread freezes an app.
- Write out three sequential async steps as nested callbacks (pseudocode) and feel the "hell". Then
write the same as sequential
suspend-style code and compare. - Describe, in your own words, the difference between blocking a thread and suspending a coroutine.
- Explain why you can run ten thousand coroutines but not ten thousand threads on a phone.
- List three things in an app you use that must be asynchronous (they wait on something). For each, say what it waits for.
- Recall an app that froze or showed "not responding". Consider whether blocking the main thread could have been the cause.
Official documentation
- Kotlin — Coroutines guide — The starting point for the whole topic.
- Kotlin — Coroutines basics — Suspending versus blocking, and the first examples.
- Android — Kotlin coroutines on Android — Why they matter specifically for Android and the main thread.
Next: suspend functions — the building block of everything here.
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