Kotlin coroutines interview questions, with answers
Coroutines are how Kotlin writes asynchronous code that reads like ordinary sequential code, and they are the standard for Android and much server-side Kotlin. Interviewers ask about them in depth: what suspending means, how the coroutine builders differ, how structured concurrency keeps coroutines from leaking, how cancellation works, and when a Flow is the right tool.
The answers below cover each of these with code compiled with kotlinc 2.3 and run on the JVM against kotlinx-coroutines 1.10; the kotlinx.coroutines imports are left out of the listings. Then take the free Kotlin diagnostic — ten questions across every Kotlin topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.What is a coroutine in Kotlin?
In short: A coroutine is a computation that can suspend and resume without blocking a thread; many coroutines can share a few threads, and they are started by builders such as launch.
A coroutine runs code that can pause at suspension points, such as delay or a network call, and resume later, possibly on another thread, while the thread itself is freed for other work. They are created by builders: runBlocking bridges ordinary code and coroutines by blocking the current thread until its coroutines finish, launch starts a coroutine that does not return a result, and async one that does. In the code, three coroutines are launched; they start once the main block reaches its end and suspends, and run in launch order, printing 0 1 2. Coroutines are a library feature built on the compiler's support for suspend functions.
fun main() = runBlocking { repeat(3) { i -> launch { print("$i ") } } // 0 1 2 }
2.What is a suspend function in Kotlin?
In short: A function marked suspend can pause without blocking the thread; it can call other suspend functions and can only be called from a coroutine or another suspend function.
The suspend modifier tells the compiler that a function may suspend. load below calls delay, which pauses the coroutine for 10 milliseconds without blocking its thread, and then continues as if nothing happened, so the code reads sequentially while the thread serves other coroutines in the meantime. The compiler transforms suspend functions into state machines that take a hidden continuation parameter, which is why they can only be called from other suspend functions or from inside a coroutine builder. Marking a function suspend does not make it run in the background; that depends on the dispatcher its coroutine uses.
suspend fun load(n: Int): Int { delay(10) return n * 2 } fun main() = runBlocking { println(load(21)) // 42 }
3.What is the difference between launch and async in Kotlin?
In short: launch starts a coroutine for its effect and returns a Job; async starts one that produces a value and returns a Deferred, whose await() gives the result.
Both builders start a coroutine concurrently with the code that called them. launch returns a Job, which can be joined to wait for completion or cancelled, but carries no result; an exception inside it propagates to its parent immediately. async returns a Deferred<T>, a Job with a result: await() suspends until the value is ready and returns it, or rethrows the coroutine's exception. Starting several async calls before awaiting any is the usual way to run independent operations in parallel and combine their results. In the code, a supplies 42 and j is a finished Job.
fun main() = runBlocking { val a = async { 6 * 7 } val j = launch { delay(5) } j.join() println(j.isCompleted) // true println(a.await()) // 42 }
4.What is structured concurrency in Kotlin?
In short: Every coroutine is started in a scope, and a scope does not complete until all its children have completed, so coroutines cannot be forgotten or leaked.
Coroutines form a tree: each one is launched in a CoroutineScope and becomes a child of that scope's job. A scope waits for all of its children before it completes, and cancelling a scope cancels every coroutine in it, which ties the lifetime of background work to something with a clear lifetime, such as a screen or a request. coroutineScope below creates a nested scope for a suspend function: both async children run concurrently, and the function returns only when both have finished. Android's viewModelScope and lifecycleScope apply the same rule to UI components. GlobalScope escapes the tree, which is why its use is discouraged.
suspend fun both(): Int = coroutineScope { val a = async { 1 } val b = async { 2 } a.await() + b.await() } fun main() = runBlocking { println(both()) // 3 }
5.How do you cancel a coroutine in Kotlin?
In short: Call cancel on its Job, often as cancelAndJoin; cancellation is cooperative, taking effect at the next suspension point or check of isActive.
j.cancel() asks a coroutine to stop, and cancelAndJoin also waits until it has. Cancellation is cooperative: the coroutine receives a CancellationException at its next suspending call, such as the delay inside the loop below, which unwinds it and runs its finally blocks. Code in a tight loop with no suspending calls must check isActive or call ensureActive() to notice cancellation. Cancelling a parent cancels its children, and timeouts are cancellations too, through withTimeout. Because CancellationException is how cancellation travels, catching every exception and swallowing it can keep a cancelled coroutine running by mistake.
fun main() = runBlocking { val j = launch { repeat(100) { delay(10) } } delay(25) j.cancelAndJoin() println(j.isCancelled) // true }
6.What is a Flow in Kotlin?
In short: A Flow is an asynchronous stream of values, built with flow { emit(...) } and consumed with collect; it is cold, so its code runs again for each collector.
A suspend function returns a single value; a Flow produces a sequence of values over time, each sent with emit, and can suspend between them. flow { ... } below emits three squares, and collect receives them one by one. Flows are cold: the builder's code does not run until something collects, and it runs anew for each collector, unlike a hot StateFlow or SharedFlow, which exists independently of collectors. Operators such as map, filter, take and debounce transform flows, and flowOn changes the dispatcher of the upstream code. Android UIs typically expose state as a StateFlow and collect it in the view.
fun nums() = flow { for (i in 1..3) emit(i * i) } fun main() = runBlocking { nums().collect { print("$it ") } // 1 4 9 }
7.How are coroutines different from threads?
In short: Coroutines are lightweight tasks scheduled by a library onto a small pool of threads; suspending one frees its thread, so thousands of coroutines cost far less than thousands of threads.
Each thread has its own large stack and is scheduled by the operating system, so a program can afford only a limited number, and a thread blocked on I/O does nothing useful while it waits. A coroutine is an object holding its state; when it suspends, its thread moves on to another coroutine, and resuming it later is a cheap library operation. The code starts ten thousand coroutines that each wait 100 milliseconds, which completes almost at once on a handful of threads; ten thousand threads sleeping would use gigabytes of stack. Coroutines still run on threads, chosen by their dispatcher, and blocking calls inside them should move to a dispatcher meant for blocking.
fun main() = runBlocking { val jobs = List(10_000) { launch { delay(100) } } jobs.joinAll() println(jobs.size) // 10000 }
How the diagnostic asks it
One question from the Kotlin bank, exactly as a sitting would show it. The bank has 4 on coroutines and 30 across Kotlin.
What does this Kotlin program print?
import kotlinx.coroutines.* fun main() = runBlocking { launch { println("B") } println("A") }
- 1B, then A
- 2A only: the program ends before the launched coroutine runs
- 3It does not compile: launch needs GlobalScope
- 4A, then Bcorrect
runBlocking runs its block in a coroutine on the main thread and does not return until every coroutine launched in its scope has completed. launch schedules a new coroutine but does not run it immediately: the current coroutine keeps going and prints A, and only when the block ends, suspending to wait for its children, does the launched coroutine get the thread and print B. So the output is A, then B. B, then A assumes launch runs its block at once. A only ignores that runBlocking waits for its children, which is structured concurrency. Inside runBlocking, launch is called on its scope, so no GlobalScope is needed.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 Kotlin questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.