Exception & Cancellation Handling in Coroutines
📖 Concept
Exception handling in coroutines is one of the most misunderstood topics. The rules are different from regular try-catch, and getting them wrong leads to silent failures or crashes.
Key rules:
- Exceptions in
launchpropagate to the parent — crash if unhandled - Exceptions in
asyncare stored and rethrown on.await() CancellationExceptionis NEVER propagated — it's used for normal cancellationCoroutineExceptionHandlercatches exceptions fromlaunchonly (notasync)- In
supervisorScope, child failures don't propagate to siblings
Exception propagation:
launch { // Exception propagates UP immediately
throw RuntimeException() // → parent Job cancelled → ALL siblings cancelled
}
async {
throw RuntimeException() // Exception stored until await() is called
}
The CancellationException trap: CancellationException is a special exception used for coroutine cancellation. If you catch it, you break structured concurrency — the coroutine won't cancel properly. ALWAYS rethrow it.
CoroutineExceptionHandler:
Only works on top-level coroutines (root coroutines started with launch). Does NOT work with async or nested coroutines.
💻 Code Example
1// Exception handling patterns23// Pattern 1: try-catch in the coroutine body4viewModelScope.launch {5 try {6 val data = repository.fetchData() // throws IOException7 _state.value = UiState.Success(data)8 } catch (e: CancellationException) {9 throw e // ALWAYS rethrow!10 } catch (e: Exception) {11 _state.value = UiState.Error(e.message ?: "Unknown error")12 }13}1415// Pattern 2: CoroutineExceptionHandler for top-level launch16val handler = CoroutineExceptionHandler { _, exception ->17 Log.e("Coroutine", "Unhandled: ${exception.message}")18 _state.value = UiState.Error(exception.message ?: "")19}20viewModelScope.launch(handler) {21 repository.fetchData() // If this throws, handler catches it22}2324// Pattern 3: Result type for clean error handling25sealed interface DataResult<out T> {26 data class Success<T>(val data: T) : DataResult<T>27 data class Error(val exception: Throwable) : DataResult<Nothing>28}2930suspend fun <T> safeApiCall(block: suspend () -> T): DataResult<T> {31 return try {32 DataResult.Success(block())33 } catch (e: CancellationException) {34 throw e35 } catch (e: Exception) {36 DataResult.Error(e)37 }38}3940// Usage41viewModelScope.launch {42 when (val result = safeApiCall { api.getUser(id) }) {43 is DataResult.Success -> _state.value = UiState.Loaded(result.data)44 is DataResult.Error -> _state.value = UiState.Failed(result.exception.message)45 }46}4748// SupervisorScope for independent operations49viewModelScope.launch {50 supervisorScope {51 // Each child handles its own errors52 val feed = async {53 try { api.getFeed() } catch (e: Exception) { emptyList() }54 }55 val profile = async {56 try { api.getProfile() } catch (e: Exception) { null }57 }58 _state.value = HomeState(feed.await(), profile.await())59 }60}
🏋️ Practice Exercise
Practice:
- Explain why catching CancellationException breaks structured concurrency
- Implement a safeApiCall wrapper that handles all exceptions properly
- What happens if an exception is thrown inside an async block but await() is never called?
- Design an error handling strategy for a ViewModel with 5 concurrent API calls
- Explain the difference between try-catch and CoroutineExceptionHandler
⚠️ Common Mistakes
Catching all exceptions including CancellationException — breaks cancellation. Use catch(e: Exception) and rethrow CancellationException.
Using CoroutineExceptionHandler with async — it doesn't work, exceptions are only rethrown on await()
Not handling exceptions in supervisorScope children — they fail silently without try-catch
Assuming try-catch around launch catches exceptions — it doesn't, the exception propagates to the parent Job
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Exception & Cancellation Handling in Coroutines. Login to unlock this feature.