This episode masters coroutines and asynchronous programming in Kotlin: suspending functions, coroutine scopes and dispatchers, launch and async, structured concurrency with exception handling, and the Flow API for reactive streams with backpressure and transformations.

Episode 9 brings you to one of the most important features of modern Kotlin: coroutines. In episode 2 you heard the general overview; now it's time to practice directly with suspending functions, scopes, dispatchers, launch, async, structured concurrency, and Flow.
Coroutines change the way you write asynchronous code: instead of nested callbacks or complex chains, you write sequential code that can be suspended without blocking a thread. This is used in Android, backend, and multiplatform development.
After this episode, you'll handle network calls, I/O operations, and data streams with code that reads like synchronous code.
A function that can be suspended is marked with the suspend keyword. When calling a slow operation like a network call, the coroutine suspends itself — the thread is freed for other work — then resumes execution when the result is ready:
suspend fun ambilData(): String {
delay(1000)
return "data siap"
}delay suspends the coroutine for 1 second without blocking the thread. A suspend function can only be called from a coroutine or another suspend function — the compiler enforces this restriction, so you can't forget to use a scope.
To run a suspending function, you need a coroutine builder like runBlocking (for main functions and tests):
import kotlinx.coroutines.*
fun main() = runBlocking {
val data = ambilData()
println(data)
}runBlocking creates a bridge between the blocking world and coroutines. In production, you don't use runBlocking for real operations — instead you use an application scope like CoroutineScope(Dispatchers.IO) whose lifecycle is managed. The examples in this episode require the kotlinx-coroutines-core dependency, which you can add to a Gradle project via ./gradlew build.
A dispatcher determines the thread where a coroutine runs. The three most common ones:
val scope = CoroutineScope(Dispatchers.Default)
fun jalankan() {
scope.launch {
val hasil = withContext(Dispatchers.IO) {
// operasi I/O: membaca file, memanggil jaringan
"hasil I/O"
}
println(hasil)
}
}Dispatchers.Default for CPU-bound computation, Dispatchers.IO for I/O operations, and Dispatchers.Main in Android for UI updates. withContext switches dispatchers in the middle of a coroutine without losing its context.
launch starts a coroutine that returns a Job — suitable for work without a result. async returns a Deferred whose result can be retrieved with await, enabling parallel execution:
fun main() = runBlocking {
val a = async { hitungBerat(1) }
val b = async { hitungBerat(2) }
println("Total: ${a.await() + b.await()}")
}async runs two independent computations in parallel, and await waits for both. This pattern is far more concise than managing threads or futures manually.
Structured concurrency guarantees that a coroutine cannot leak out of its parent scope: if the parent is cancelled, all children are cancelled too. This prevents the background-work leaks that often cause memory bugs:
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scope.launch {
try {
panggilApi()
} catch (e: Exception) {
println("Gagal: ${e.message}")
}
}SupervisorJob ensures one child's failure does not cancel its siblings. Combining try-catch inside the coroutine with a SupervisorJob on the scope gives you granular control over failures without collapsing the whole hierarchy.
For global handling, use a CoroutineExceptionHandler on the scope:
val handler = CoroutineExceptionHandler { _, e ->
println("Coroutine gagal: ${e.message}")
}
val scope = CoroutineScope(SupervisorJob() + handler)The handler catches unhandled exceptions from coroutines in that scope. The combination of structured concurrency, SupervisorJob, and a handler makes failure behavior predictable.
Flow is the official recommendation for asynchronous streams in Kotlin. Flow is cold: the production of values only starts when it is collected. Transformations like map and filter work just like on collections:
import kotlinx.coroutines.flow.*
fun hitungFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(500)
emit(i * i)
}
}
fun main() = runBlocking {
hitungFlow()
.filter { it % 2 == 0 }
.collect { println(it) }
}emit produces a value, and collect consumes it. The filter operator above keeps only even values. Flow also supports map, flatMapConcat, take, and other operators for building data-processing pipelines.
In Android, Flow is used for the data layer, with StateFlow as a reactive state holder. In the backend, Flow is great for streaming data and operations that respond to backpressure. You'll see Flow used with coroutines again in episodes 10 and 11.
Episode 9 unlocked the power of Kotlin concurrency: suspending functions that don't block threads, scopes and dispatchers for controlling execution, launch and async for parallel tasks, structured concurrency for cancellation safety, and Flow for reactive streams.
The key takeaways:
suspend function suspends without blocking a thread.runBlocking for main and tests; an application scope for production.Dispatchers.Default for CPU, Dispatchers.IO for I/O.launch for tasks without a result; async plus await for parallel results.In episode 10 we'll discuss Kotlin for Android development — setting up an Android project with Kotlin, Android-specific features, ViewModel and LiveData with coroutines, and the basics of Jetpack Compose with the Kotlin DSL.