Coroutine Exceptions & Error Handling Quiz
KOTLIN › Coroutines
Why does wrapping scope.launch { } in a try/catch never catch the coroutine is exception?
- launch returns once the body is scheduled, so the try block has already exited
- Coroutine exceptions are Error subclasses and are not matched by catch (e: Exception)
- The exception is thrown on a different thread, and try/catch is thread-local
- launch catches the exception internally and reports it only through logging
Answer: launch returns once the body is scheduled, so the try block has already exited
The builder schedules the body and returns. By the time the throw executes the try has exited, so the exception fails the Job and travels up the job hierarchy instead.
Where must a CoroutineExceptionHandler be installed to fire?
- On a coroutine that is a root of its scope, or on the scope context itself
- On any coroutine in the hierarchy, since handlers are inherited downward
- On the innermost coroutine, closest to where the exception is thrown
- On the dispatcher, using Dispatchers.IO + handler, before any coroutine starts
Answer: On a coroutine that is a root of its scope, or on the scope context itself
Only a root coroutine consults its handler. A nested child reports its failure upward instead, so a handler in its context is ignored and the app still crashes.
What does this print?
- caught: java.lang.RuntimeException: boom
- Nothing, and the app crashes: the inner coroutine is not a root
- caught twice, once for each coroutine in the hierarchy
- A compile error, since a handler cannot be passed to a nested launch
Answer: Nothing, and the app crashes: the inner coroutine is not a root
The inner launch is a child, so its failure propagates to the parent rather than being handled locally. Moving the handler to the outer launch, or to the scope, makes it fire.
Which statement about CancellationException is correct?
- It is never delivered to a CoroutineExceptionHandler and never fails a parent
- It fails the parent scope like any other exception, but skips sibling cancellation
- It is delivered to the handler only when thrown from a root coroutine
- It is an Error subclass, so a broad catch (e: Exception) will not match it
Answer: It is never delivered to a CoroutineExceptionHandler and never fails a parent
It signals intentional shutdown rather than failure, so it is excluded from propagation entirely. It is an ordinary Exception subclass though, which is exactly why a broad catch swallows it by accident.
Inside coroutineScope, one of three async calls throws. What does the caller see?
- The original exception rethrown, with the other two coroutines cancelled
- Partial results, with the failed call reported separately
- A CompositeException listing every call that was in flight
- A suspended call that never returns, because one deferred was never awaited
Answer: The original exception rethrown, with the other two coroutines cancelled
The failure cancels the scope and therefore the siblings, then the scope rethrows. Making one call optional means isolating it, for example with runCatching inside its own async.
Two sibling coroutines throw at nearly the same moment. How does the second exception reach you?
- Attached to the first as a suppressed exception
- Wrapped with the first inside a CompositeException
- Discarded entirely, since only one exception can propagate
- Delivered separately to the CoroutineExceptionHandler a moment later
Answer: Attached to the first as a suppressed exception
The first failure to reach the parent is thrown and later ones are recorded in its suppressed array, which is why logging stackTraceToString() rather than message matters.
What is wrong with this retry helper?
- It retries on cancellation, so leaving the screen triggers more attempts
- It cannot call delay inside a catch block, so the helper will not compile
- It retries too few times, since repeat excludes the final attempt
- It will loop forever, because the exception is never rethrown
Answer: It retries on cancellation, so leaving the screen triggers more attempts
The broad catch matches CancellationException, so cancelling the caller looks like a retryable failure. A CancellationException clause that rethrows must come first.
Under supervisorScope, why does a handler on a direct child fire when the same code under coroutineScope ignores it?
- Because a supervisor refuses the failure, making each direct child a root for handling
- Because supervisorScope copies handlers down to all descendants
- Because supervisors first convert all of their child exceptions into plain cancellations
- Because handlers only work on scopes that never cancel their children
Answer: Because a supervisor refuses the failure, making each direct child a root for handling
Handlers apply to coroutines whose failure has nowhere further to go. Declining to absorb the failure is exactly what makes a supervised child a root.
How far does supervisorScope isolation extend?
- To direct children only: a grandchild failure still cancels its own sibling group
- To the entire subtree beneath it, at any depth
- To direct children and grandchildren, but no deeper
- To nothing at all: it only changes which exception is reported, not what gets cancelled
Answer: To direct children only: a grandchild failure still cancels its own sibling group
A grandchild reports to its own ordinary parent, so that inner group cancels together. The supervisor only stops the failure spreading to the outer siblings.
What is the danger of runCatching { api.fetch() } inside a coroutine?
- It catches Throwable, including CancellationException, breaking cancellation
- It cannot wrap suspending calls, so the code will not compile
- It rethrows on the calling thread rather than returning a Result
- It converts the exception into a null result, hiding the failure from the caller
Answer: It catches Throwable, including CancellationException, breaking cancellation
Swallowing the cancellation signal means a cancelled coroutine keeps going as though the call merely failed. A typed wrapper that rethrows CancellationException first avoids it.
A root async throws and nothing ever calls await(). What happens?
- The exception is stored in the Deferred and silently lost
- The CoroutineExceptionHandler fires once the coroutine completes
- The process crashes when the Deferred is garbage collected
- The exception is rethrown on the thread that created the coroutine
Answer: The exception is stored in the Deferred and silently lost
Handlers are ignored for async, and with no parent watching, await() is the only surfacing path. Never calling it loses the failure entirely.
Which failure belongs in a try/catch rather than a CoroutineExceptionHandler?
- An IOException from a network call the UI should show a retry button for
- An unexpected NullPointerException deep inside a third-party library call
- Any failure you want reported to a crash reporter
- A failure in a coroutine started from a long-lived application scope
Answer: An IOException from a network call the UI should show a retry button for
A handler is a crash-reporting tool of last resort. A failure with a meaningful response belongs where the code that knows the response can see it.
What is TimeoutCancellationException and how does it behave?
- A CancellationException subclass, so it cancels only its block and never fails the scope
- A checked exception that must be declared by every single function that calls withTimeout
- A plain RuntimeException that fails the enclosing scope like any other
- An Error that cannot be caught, only observed via a handler
Answer: A CancellationException subclass, so it cancels only its block and never fails the scope
Because it is a CancellationException, it obeys every rule about cancellation not being a failure, which is why withTimeout scopes its effect to its own block.
Why does catching an exception inside one child not stop the parent scope from being cancelled?
- Because catching handles your own failure and has no say over a sibling failing
- Because a caught exception is rethrown automatically once the block exits
- Because the parent re-runs each child to confirm the failure before it cancels anything
- Because try/catch inside a coroutine is ignored unless the scope is supervised
Answer: Because catching handles your own failure and has no say over a sibling failing
Cancellation of the scope is driven by whichever child actually failed. Handling your own exception keeps you alive but does nothing about a sibling that did not.
What is the recommended layering for turning failures into UI state?
- Repositories return Result or a sealed type; ViewModels map that to state
- ViewModels wrap every repository call in try/catch and log the failure
- Repositories throw, and a global CoroutineExceptionHandler renders the error screen
- The UI layer catches exceptions directly inside the composable that renders
Answer: Repositories return Result or a sealed type; ViewModels map that to state
Making failure part of the return type means a caller cannot forget it, and the ViewModel needs no try/catch. A handler cannot render an error screen because it has no idea which screen was loading.
How does exception delivery differ between launch and async?
- Both encapsulate and rethrow the exception only at a join() or await() call
- Neither one propagates its uncaught exception up to the parent coroutine scope
- launch rethrows inside await(), while async just logs it to the console
- launch propagates to its scope immediately; async holds it for await()
Answer: launch propagates to its scope immediately; async holds it for await()
launch treats an uncaught exception as a failure and propagates it up the scope right away, while async stores it in the Deferred so it surfaces when you call await().