-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
181: Structured Concurrency Pitfalls and How to Avoid Them
By now, you're comfortable launching coroutines and using async to get things done in parallel. But structured concurrency isn't just about keeping things tidy; it's about how failures propagate. If you don't understand the hierarchy, you'll find yourself staring at a screen wondering why a minor network glitch in a background task just killed your entire user session.
Let's build a UserDashboardService. The goal is to fetch three things: user profile data, a list of recent orders, and a notification count. The profile and orders are critical, but the notification count is "nice to have." If notifications fail, we still want to show the profile and orders.
Gathering user data in parallel
I'll start by wrapping these calls in a coroutineScope. This ensures that the aggregateDashboardData function doesn't return until all the child jobs are finished. It's the standard way to group concurrent work.
suspend fun aggregateDashboardData(): DashboardData = coroutineScope {
val profile = async { fetchUserProfile() }
val orders = async { fetchRecentOrders() }
val notifications = async { fetchNotificationCount() }
DashboardData(profile.await(), orders.await(), notifications.await())
}
This looks clean, right? It's concise and it's parallel. But here is where I usually make my first mistake when I'm rushing a feature.
When one failure takes down the whole ship
Imagine fetchNotificationCount() throws an IOException because the notification microservice is down. In the code above, I'm using coroutineScope. In Kotlin, coroutineScope is "fail-fast." If any child fails, the scope immediately cancels all other children and throws the exception up to the caller.
I tested this and realized that even though fetchUserProfile() had already succeeded, the failure of the notification call cancelled the entire operation. My user sees an error page instead of their profile and orders just because a tiny notification badge failed to load. That's a terrible user experience.
Isolating the blast radius with supervisorScope
To fix this, I need to change the relationship between the parent and the children. I want a "supervisor" relationship where a failure in one child doesn't automatically kill the others. I'll swap coroutineScope for supervisorScope.
But there's a catch: async inside a supervisorScope doesn't propagate the exception to the parent immediately; it encapsulates it in the Deferred object. I have to handle the exception during the await() call.
suspend fun aggregateDashboardData(): DashboardData = supervisorScope {
val profile = async { fetchUserProfile() }
val orders = async { fetchRecentOrders() }
val notifications = async {
try {
fetchNotificationCount()
} catch (e: Exception) {
0 // Fallback to zero notifications on error
}
}
DashboardData(
profile = profile.await(),
orders = orders.await(),
notifications = notifications.await()
)
}
Now, if fetchNotificationCount blows up, it only affects its own async block. The profile and orders calls are left untouched. I've isolated the failure to the specific part of the UI that can actually handle it.
Avoiding the GlobalScope escape hatch
When developers run into these cancellation issues, they're often tempted to just use GlobalScope.launch { ... } to "fire and forget" the problematic task. I've seen this in too many code reviews.
Don't do it. Using GlobalScope breaks the entire contract of structured concurrency. You lose the ability to cancel the operation when the user leaves the screen, and you create memory leaks that are a nightmare to debug in production. If you need a task to outlive the current scope, inject a custom CoroutineScope tied to the application lifecycle (like a ViewModelScope or a custom ServiceScope), but never reach for GlobalScope just to avoid a CancellationException.
📋 Practical Task
Fixing the Fragile User Dashboard Aggregator
You have been handed a piece of code for a WeatherDashboard that fetches data from three sources: currentTemperature, windSpeed, and localNews. Currently, the code uses coroutineScope, meaning if the localNews API (which is flaky) fails, the user doesn't even get to see the temperature.
Your Task: Modify the following function so that:
- The failure of
fetchLocalNews()does not cancel the temperature or wind speed requests. - If
fetchLocalNews()fails, it returns a default string:"News currently unavailable". - The overall function still waits for all three requests to complete (or fail) before returning.
suspend fun getWeatherData(): WeatherData = coroutineScope {
val temp = async { fetchTemperature() }
val wind = async { fetchWindSpeed() }
val news = async { fetchLocalNews() }
WeatherData(temp.await(), wind.await(), news.await())
}There are no comments for now.