Coroutines Basics Quiz
KOTLIN › Coroutines
What best describes what happens when a coroutine hits a suspension point inside a suspend function?
- The thread blocks until the operation finishes and then resumes
- The coroutine is moved into a new process to continue running
- The coroutine suspends, and the thread is freed for other work
- The JVM creates a fresh OS thread for that coroutine
Answer: The coroutine suspends, and the thread is freed for other work
Suspension pauses the coroutine without blocking; the underlying thread is released to do other work and the coroutine resumes later.
Which builder returns a Deferred that you await for a result?
- async
- launch
- runBlocking
- withContext
Answer: async
async returns Deferred<T> and you call await() to get the value; launch returns a Job with no result.
Under the hood, how does the Kotlin compiler implement suspend functions?
- By wrapping each suspending call inside a synchronized lock block automatically
- By scheduling each suspend function on its own dedicated background thread
- By a CPS transform into a state machine with a hidden Continuation parameter
- By using Java Virtual Threads exclusively under the hood for every call
Answer: By a CPS transform into a state machine with a hidden Continuation parameter
The compiler rewrites a suspend function using CPS, adding a Continuation parameter and compiling the body into a resumable state machine.
How does delay differ from Thread.sleep inside a coroutine?
- Both block the underlying carrier thread for the entire given duration
- Thread.sleep is the coroutine-friendly, non-blocking way to pause a job
- delay suspends without blocking, while Thread.sleep blocks the thread
- delay can only be called from inside a runBlocking builder block
Answer: delay suspends without blocking, while Thread.sleep blocks the thread
delay is a suspending function that releases the thread while the coroutine waits, whereas Thread.sleep blocks the thread for the whole duration, wasting it.
From where can a suspend function be called?
- From any ordinary function, just like a normal call in Kotlin code
- Only from another suspend function, or inside a coroutine builder
- Only from functions marked with @Composable in a Compose UI layer
- Only from code running on Dispatchers.Default in the background
Answer: Only from another suspend function, or inside a coroutine builder
Suspend functions need a continuation, so they can only be invoked from another suspend function or from a coroutine started by a builder like launch, async, or runBlocking.
What is the intended use of runBlocking?
- To bridge blocking code to coroutines in main() or tests, blocking the thread
- To run a coroutine on the main thread without ever actually blocking it
- As the standard way to start coroutines from an Android Activity or ViewModel
- To switch dispatchers cleanly from inside a running suspend function
Answer: To bridge blocking code to coroutines in main() or tests, blocking the thread
runBlocking blocks the current thread until its body completes, which is useful for main functions and tests but is the wrong tool inside UI code, where it would freeze the thread.
Which function should NOT be marked suspend?
- suspend fun fetchPosts(): List<Post> which calls a Retrofit API endpoint
- suspend fun readFile(path: String): ByteArray which reads bytes from disk using withContext(Dispatchers.IO) around the blocking call
- suspend fun formatName(first: String, last: String): String which returns "$first $last" and calls no suspending functions
- suspend fun delay500() which calls delay(500) to pause before retrying an operation
Answer: suspend fun formatName(first: String, last: String): String which returns "$first $last" and calls no suspending functions
formatName does pure string concatenation -- nothing inside it suspends. Marking it suspend adds coroutine machinery overhead for no benefit. The others genuinely suspend: fetchPosts waits for a network response, readFile uses withContext, and delay500 calls delay.
What does a Job represent?
- The lifetime of running work, with handles to cancel it and wait for it
- The result value produced by a coroutine once it finishes
- The specific thread that a coroutine has been assigned to by its dispatcher
- The queue of suspending calls a coroutine still has to make
Answer: The lifetime of running work, with handles to cancel it and wait for it
A Job is about lifetime: started, finishing, cancelled. Deferred is the variant that also carries a result, which is why it extends Job rather than replacing it.
Immediately after job.cancel(), job.isCompleted is false. Why?
- The job is Cancelling: it still has to unwind and wait for its children
- cancel() only propagates down to the children, and never to the job itself
- isCompleted is reserved for jobs that finished successfully
- The cancellation flag is only read at the next dispatch
Answer: The job is Cancelling: it still has to unwind and wait for its children
Cancellation is cooperative and asynchronous. The job passes through Cancelling while finally blocks run and children unwind, and only then reaches Cancelled, where isCompleted becomes true.
Which is true of a cancelled Job?
- isCompleted is true and isCancelled is true
- isCompleted is false, because it never finished normally
- isActive remains true until the job is garbage collected
- All three flags are false, since the job never reached a terminal state
Answer: isCompleted is true and isCancelled is true
A cancelled coroutine is finished, just not successfully. Using isCompleted alone to mean success is therefore a bug: pair it with isCancelled to tell them apart.
How do join() and await() differ on failure?
- join() returns normally regardless; await() rethrows the coroutine exception
- Both rethrow, but join() first wraps the exception in a JobCancellationException
- Neither rethrows: failures only reach a CoroutineExceptionHandler
- join() rethrows and await() returns a null result instead
Answer: join() returns normally regardless; await() rethrows the coroutine exception
join waits for completion however it completed, so it stays silent. await waits for a value and therefore has to surface the failure, which is why the same coroutine can look quiet or loud depending on which you called.
Roughly how long does this take, given each call takes 300ms?
- About 600ms, because awaiting on the same line makes the calls sequential
- About 300ms, because both async coroutines overlap
- About 300ms, but only when the dispatcher has multiple threads
- It deadlocks, because a deferred cannot be awaited on the line that created it
Answer: About 600ms, because awaiting on the same line makes the calls sequential
The second async is not created until the first await returns, so nothing overlaps. Concurrency requires starting both deferreds before awaiting either.
Which statement about suspend fun is correct?
- It is a function, not a coroutine: it has no Job and nothing to cancel
- It starts a new coroutine each time it is called
- It always runs on a background dispatcher chosen by the runtime
- It creates a child of the calling coroutine that can be cancelled independently
Answer: It is a function, not a coroutine: it has no Job and nothing to cancel
Only builders create coroutines. A suspend function runs inside its caller, on its caller dispatcher, and completes before the caller continues, which is why it has no lifetime of its own.
What is wrong with this refresh function?
- The hand-built scope has no lifetime, so nothing cancels the work it starts
- It will not compile, since a scope cannot be created inside a suspend function
- It runs on the wrong dispatcher, because IO is not valid for a launch
- It returns before the sync starts, so the sync never actually runs
Answer: The hand-built scope has no lifetime, so nothing cancels the work it starts
The caller has no idea this scope exists and nothing cancels it, so syncEverything outlives the screen. coroutineScope inherits the calling job instead, so cancellation propagates and the function waits for its children.
Why is GlobalScope.launch discouraged on Android?
- It has no lifetime, so nothing cancels the work and it outlives the screen
- It runs on the main thread, so it blocks rendering
- It cannot call suspend functions, only blocking ones
- It is slower than a scoped launch, because it allocates a brand new dispatcher
Answer: It has no lifetime, so nothing cancels the work and it outlives the screen
Every structured-concurrency guarantee is switched off: the work is unowned, keeps its captured references alive, and carries on after the user has moved on. That is why it is marked DelicateCoroutinesApi.
Work must outlive a screen but not process death. What should own it?
- An injected application-scoped CoroutineScope built on a SupervisorJob
- GlobalScope, since the requirement is explicitly not tied to any one screen
- WorkManager, because any work outliving a screen must be persisted
- A viewModelScope on a ViewModel retained across navigation
Answer: An injected application-scoped CoroutineScope built on a SupervisorJob
An application scope gives the lifetime you want while staying cancellable, injectable and testable. WorkManager is the answer only when the work must survive the process being killed.