Why i can't use try/catch to catch exception in Kotlin coroutine?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
You can use try/catch with Kotlin coroutines, but only where the exception is actually observed. The confusing part is that coroutine builders such as launch and async surface failures differently, so a try/catch around the wrong call site can look like it does nothing.
async and launch Do Not Report Failures the Same Way
async stores its result, including failure, inside a Deferred. You usually observe that failure when you call await().
launch does not return a value. If it fails, the exception is treated as an uncaught coroutine failure and is propagated to the parent scope.
Here is the key difference:
await() rethrows the failure from async, so try/catch works there. join() only waits for launch to finish. It does not rethrow the original exception for you.
Catch Inside the launch Body When Needed
If the failing code is inside a launch coroutine and you want local handling, the usual fix is to wrap the body itself:
This works because the exception is thrown inside the scope surrounded by try/catch.
Use CoroutineExceptionHandler for Top-Level launch
For top-level launch coroutines, CoroutineExceptionHandler is often the right tool for centralized logging or fallback behavior.
This is not a substitute for local recovery logic, but it is how uncaught launch failures are typically observed at a scope boundary.
Structured Concurrency Changes What Gets Cancelled
Coroutines do not fail in isolation unless the scope structure allows them to. In structured concurrency, a failing child can cancel its parent and siblings unless supervision changes that behavior.
That is why exception handling is not only about syntax. It is also about coroutine hierarchy.
If one child failure should not cancel the rest, use supervision:
Without supervision, a parent-child failure chain can make it look as though try/catch "did not work," when the real issue is failure propagation through the scope tree.
A Useful Debugging Rule
When coroutine exception handling feels inconsistent, ask two questions:
- which builder created the coroutine
- where does this builder surface failure
If the answer is async, look at await(). If the answer is launch, look at the coroutine body, the parent scope, or the exception handler attached to that scope. That mental model is usually enough to explain why a try/catch block appears to be ignored.
Common Pitfalls
- Wrapping
launchcreation intry/catchand expectingjoin()to rethrow the exception. - Forgetting that
await()is whereasyncfailure usually becomes observable. - Catching too far away from the actual suspension point or coroutine body.
- Ignoring parent-child cancellation behavior in structured concurrency.
- Using
GlobalScopecasually and then losing track of where failures are supposed to go.
Summary
- '
try/catchworks with coroutines, but only where the exception is surfaced.' - '
asyncfailures are typically caught aroundawait().' - '
launchfailures are usually handled inside the coroutine body or through aCoroutineExceptionHandler.' - Coroutine hierarchy affects exception behavior just as much as syntax does.
- When error handling seems broken, check both the builder type and the parent-child scope structure.

