0
0
Android Kotlinmobile~15 mins

Exception handling in coroutines in Android Kotlin - Deep Dive

Choose your learning style9 modes available
Overview - Exception handling in coroutines
What is it?
Exception handling in coroutines is the way to catch and manage errors that happen while running tasks asynchronously in Kotlin coroutines. Coroutines let your app do work in the background without freezing the screen. Handling exceptions properly means your app can respond to problems without crashing or freezing.
Why it matters
Without proper exception handling in coroutines, your app might crash unexpectedly or behave unpredictably when something goes wrong during background work. This can frustrate users and cause data loss. Good exception handling keeps your app stable and responsive, even when errors happen.
Where it fits
Before learning this, you should understand basic Kotlin syntax and how coroutines work for asynchronous tasks. After this, you can learn advanced coroutine topics like structured concurrency, cancellation, and custom coroutine contexts.
Mental Model
Core Idea
Exception handling in coroutines is about catching errors in asynchronous tasks so your app can handle failures gracefully without crashing.
Think of it like...
Imagine you are cooking multiple dishes at once. If one dish burns, you want to notice it and fix it without throwing away the whole meal. Exception handling in coroutines is like watching each dish carefully and handling problems as they happen.
Coroutine Exception Flow
┌───────────────┐
│ Coroutine Run │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│  Exception?   │──No──▶ Continue
└──────┬────────┘
       │Yes
       ▼
┌───────────────┐
│ Catch & Handle│
└───────────────┘
Build-Up - 7 Steps
1
FoundationBasics of Kotlin Coroutines
🤔
Concept: Introduce what coroutines are and how they run asynchronous code.
Coroutines let you run code that takes time (like network calls) without freezing the app. You start a coroutine with launch or async builders inside a CoroutineScope. They run in the background and let the main thread stay free.
Result
You can run tasks like downloading data without blocking the app screen.
Understanding coroutines as lightweight background workers is key before handling their errors.
2
FoundationWhat Are Exceptions in Kotlin
🤔
Concept: Explain what exceptions are and how Kotlin normally handles them.
Exceptions are errors that happen during code execution, like dividing by zero or network failure. Normally, you catch exceptions with try-catch blocks to prevent crashes.
Result
You learn to protect your code from unexpected crashes by catching errors.
Knowing basic exception handling is essential before applying it to coroutines.
3
IntermediateTry-Catch Inside Coroutines
🤔Before reading on: Do you think try-catch works the same inside coroutines as in regular code? Commit to your answer.
Concept: Learn how to use try-catch blocks inside coroutine code to catch exceptions.
You can wrap suspend functions or coroutine code inside try-catch to catch exceptions. For example: try { val data = fetchData() } catch (e: Exception) { handleError(e) } This catches errors thrown inside the coroutine block.
Result
Exceptions inside the coroutine are caught and handled without crashing the app.
Knowing that try-catch works inside coroutines helps you handle errors locally where they happen.
4
IntermediateCoroutineExceptionHandler Usage
🤔Before reading on: Does CoroutineExceptionHandler catch exceptions from all coroutine builders? Commit to your answer.
Concept: Introduce CoroutineExceptionHandler, a special handler for uncaught exceptions in coroutines.
CoroutineExceptionHandler is a context element you add to a coroutine scope to catch uncaught exceptions. It only catches exceptions from launch coroutines, not async. Example: val handler = CoroutineExceptionHandler { _, e -> println("Caught $e") } launch(handler) { throw Exception("Error") } This catches exceptions that bubble up.
Result
Uncaught exceptions in launch coroutines are caught by the handler, preventing crashes.
Understanding CoroutineExceptionHandler helps manage errors globally for coroutines launched in a scope.
5
IntermediateHandling Exceptions in async Coroutines
🤔Before reading on: Will CoroutineExceptionHandler catch exceptions from async coroutines? Commit to your answer.
Concept: Explain that async coroutines handle exceptions differently and require explicit handling.
async coroutines return Deferred objects. Exceptions inside async are stored and only thrown when you call await(). You must catch exceptions around await(), like: val deferred = async { throw Exception("Fail") } try { deferred.await() } catch (e: Exception) { handleError(e) } CoroutineExceptionHandler does NOT catch async exceptions.
Result
Exceptions from async coroutines are caught when awaiting the result, avoiding crashes.
Knowing async exceptions are deferred prevents missing errors and unexpected crashes.
6
AdvancedStructured Concurrency and Exception Propagation
🤔Before reading on: Do exceptions in child coroutines always cancel their parent scope? Commit to your answer.
Concept: Learn how exceptions propagate in coroutine hierarchies and how structured concurrency manages cancellation.
In structured concurrency, child coroutines run inside a parent scope. If a child throws an exception, it cancels the whole scope and all siblings. For example: val scope = CoroutineScope(Dispatchers.Default) scope.launch { launch { throw Exception("Child fail") } launch { delay(1000); println("Sibling") } } The sibling coroutine is cancelled when the first child fails.
Result
Exceptions in one child cancel the whole scope, stopping other coroutines.
Understanding this prevents unexpected cancellations and helps design robust coroutine hierarchies.
7
ExpertSupervisorJob and Exception Isolation
🤔Before reading on: Does SupervisorJob cancel sibling coroutines on child failure? Commit to your answer.
Concept: Introduce SupervisorJob to isolate exceptions so one child failure doesn't cancel siblings.
SupervisorJob is a special job that lets child coroutines fail independently. If one child fails, siblings continue running. Example: val supervisor = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + supervisor) scope.launch { launch { throw Exception("Fail") } launch { delay(1000); println("Still running") } } Here, the second child runs despite the first child's failure.
Result
Child coroutine failures don't cancel siblings, improving fault tolerance.
Knowing SupervisorJob helps build resilient apps where one failure doesn't break everything.
Under the Hood
Coroutines run on lightweight threads managed by the Kotlin runtime. When an exception occurs inside a coroutine, it is either caught locally or propagated up the coroutine hierarchy. For launch coroutines, uncaught exceptions are sent to the CoroutineExceptionHandler or the parent job. For async coroutines, exceptions are stored and thrown on await(). The coroutine context manages this flow and cancellation signals.
Why designed this way?
This design separates error handling for fire-and-forget tasks (launch) and deferred results (async). It prevents silent failures and allows structured concurrency to manage cancellation cleanly. Alternatives like global try-catch would be less flexible and could cause hidden bugs.
Coroutine Exception Flow Internals
┌───────────────┐
│ Coroutine Run │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Exception?    │
└──────┬────────┘
       │Yes
       ▼
┌───────────────┐       ┌─────────────────────┐
│ Is async?     │──Yes─▶│ Store exception in   │
└──────┬────────┘       │ Deferred, throw on   │
       │No              │ await()              │
       ▼                └─────────────────────┘
┌─────────────────────────────┐
│ CoroutineExceptionHandler?  │
└──────┬──────────────────────┘
       │Yes
       ▼
┌───────────────┐
│ Handle exception │
└──────┬────────┘
       │No
       ▼
┌───────────────┐
│ Cancel parent & propagate │
└───────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does CoroutineExceptionHandler catch exceptions from async coroutines? Commit to yes or no.
Common Belief:CoroutineExceptionHandler catches all coroutine exceptions, including async.
Tap to reveal reality
Reality:CoroutineExceptionHandler only catches uncaught exceptions from launch coroutines. Async exceptions must be caught when calling await().
Why it matters:Assuming it catches async exceptions leads to uncaught errors crashing the app unexpectedly.
Quick: If one child coroutine fails, do sibling coroutines always keep running? Commit to yes or no.
Common Belief:Child coroutine failures do not affect siblings; they run independently by default.
Tap to reveal reality
Reality:By default, child failures cancel the entire parent scope, stopping siblings unless SupervisorJob is used.
Why it matters:Not knowing this causes unexpected cancellations and lost work in sibling coroutines.
Quick: Can you catch coroutine exceptions with a simple try-catch around launch? Commit to yes or no.
Common Belief:Wrapping launch in try-catch catches all exceptions inside the coroutine.
Tap to reveal reality
Reality:Try-catch outside launch does not catch exceptions inside the coroutine body; you must catch inside the coroutine or use CoroutineExceptionHandler.
Why it matters:Misusing try-catch leads to uncaught exceptions and app crashes.
Quick: Does async start running immediately when called? Commit to yes or no.
Common Belief:Async coroutines start running immediately upon creation.
Tap to reveal reality
Reality:Async coroutines start immediately but exceptions are deferred until await() is called.
Why it matters:Misunderstanding this delays error detection and handling, causing bugs.
Expert Zone
1
CoroutineExceptionHandler only handles exceptions from launch coroutines and ignores exceptions from async until await is called.
2
SupervisorJob changes the default cancellation behavior, allowing child coroutines to fail independently without cancelling siblings.
3
Exception propagation in nested coroutine scopes can be controlled by combining CoroutineExceptionHandler, SupervisorJob, and custom CoroutineScopes.
When NOT to use
Avoid using CoroutineExceptionHandler for async coroutines; instead, handle exceptions explicitly with try-catch around await(). For fire-and-forget tasks, use launch with CoroutineExceptionHandler. For complex error isolation, use SupervisorJob. Alternatives include using Result wrappers or sealed classes for error handling.
Production Patterns
In production, developers use CoroutineExceptionHandler at the application or ViewModel scope to log or report uncaught errors. They use try-catch inside suspend functions for expected errors like network failures. SupervisorJob is used in UI layers to prevent one failing task from cancelling others. Async exceptions are always handled with try-catch around await to avoid crashes.
Connections
Structured Concurrency
Exception handling in coroutines builds on structured concurrency principles.
Understanding how exceptions propagate in coroutine hierarchies clarifies how structured concurrency manages task lifecycles and cancellations.
Error Handling in Functional Programming
Both use explicit error handling patterns to avoid crashes and manage failures gracefully.
Knowing functional error handling patterns like Result or Either helps design coroutine error handling that avoids exceptions and improves reliability.
Project Management Risk Handling
Exception handling in coroutines is like managing risks in projects by isolating failures and preventing total collapse.
Seeing error handling as risk management helps appreciate why SupervisorJob isolates failures and why catching exceptions early prevents cascading problems.
Common Pitfalls
#1Assuming try-catch outside launch catches coroutine exceptions.
Wrong approach:try { launch { throw Exception("Error") } } catch (e: Exception) { println("Caught") }
Correct approach:launch { try { throw Exception("Error") } catch (e: Exception) { println("Caught") } }
Root cause:Misunderstanding that launch starts a new coroutine and exceptions inside it don't propagate to the outer try-catch.
#2Using CoroutineExceptionHandler expecting it to catch async exceptions.
Wrong approach:val handler = CoroutineExceptionHandler { _, e -> println("Caught") } val deferred = async(handler) { throw Exception("Fail") } deferred.await()
Correct approach:val deferred = async { throw Exception("Fail") } try { deferred.await() } catch (e: Exception) { println("Caught") }
Root cause:Not knowing async exceptions are deferred until await and not caught by CoroutineExceptionHandler.
#3Not using SupervisorJob when independent child failure is needed.
Wrong approach:val scope = CoroutineScope(Dispatchers.Default) scope.launch { throw Exception("Fail") } scope.launch { println("Runs") }
Correct approach:val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) scope.launch { throw Exception("Fail") } scope.launch { println("Runs") }
Root cause:Ignoring default cancellation behavior where one child failure cancels siblings.
Key Takeaways
Exception handling in coroutines is essential to keep apps stable during asynchronous work.
Try-catch works inside coroutine bodies but not outside launch or async builders.
CoroutineExceptionHandler catches uncaught exceptions only from launch coroutines, not async.
Async coroutine exceptions are deferred until await is called and must be caught there.
SupervisorJob allows child coroutines to fail independently without cancelling siblings, improving fault tolerance.