The Rules of the Kitchen
In Part 1, we saw how launch
and async
allow us to perform multiple tasks concurrently (like a chef delegating orders).
The kitchen is busy, and food is getting out faster. Professional kitchen requires more than just concurrent cooks.
It needs structure, management, and protocols when things go wrong.
Consider these scenarios:
- A customer cancels their order halfway through. Do we keep cooking their meal?
- A critical piece of equipment (like an oven) fails. Does the entire kitchen shut down?
- Specific tasks need to be done at specific stations (chopping vs plating).
These are the exact problems that CoroutineScope, Dispatchers, and the principle of Structured Concurrency solve. They are the management system that turns a chaotic collection of tasks into a professional and resilient operation.
The Kitchen Stations: Dispatchers
So far, our coroutines have been running on some background thread, but we haven’t controlled which one. Dispatchers are like the special stations in a kitchen. They tell a coroutine which thread it should run on.
Kotlin gives us three primary dispatchers:
Dispatchers.Main
: The front of the house (where food is plated and served to the customer). This is the UI thread (on Android). Use it for any task that touches the user interface. It’s optimized for very short, fast operations. Never block this thread!Dispatchers.IO
: The storage. This is for I/O-intensive work, like making network calls, reading from a database, or accessing files. It maintains a large pool of threads designed for tasks that spend most of their time waiting.Dispatchers.Default
: The main prep area. This is for CPU-intensive work, like sorting a huge list, doing complex calculations, or parsing large JSON objects. Its thread pool is sized to the number of CPU cores.
To move a task between stations, we use the withContext
function.
|
|
Output:
|
|
withContext
is a suspend
function that lets us switch to a different context for a specific block of code. After code is finished, it switches back.
This is a fundamental pattern for async programming with coroutines.

The Head Chef: CoroutineScope
and Structured Concurrency
If Dispatchers
are the stations, who is in charge of all the cooks? This is the job of the CoroutineScope
.
A scope is like a head chef for a set of coroutines. It manages their overall lifecycle.
This brings us to the principle of Structured Concurrency. New coroutines can only be launched within a scope. Scope defines their lifetime. When the scope’s lifetime ends, all coroutines within it are automatically cancelled. This prevents coroutines from leaking (e.g. continuing to fetch data for a screen that’s no longer visible).
Framework-Provided Scopes
On Android, you get pre-made scopes tied to component lifecycles, which you should almost always use:
viewModelScope
: Tied to aViewModel
. It cancels all coroutines when theViewModel
is cleared. This is the default choice in Android.lifecycleScope
: Tied to an Activity or Fragment’sLifecycle
. Useful for work that needs to align with specific lifecycle events.
Warning: Avoid
GlobalScope
GlobalScope
is like a rogue chef who works independently and never goes home when the kitchen closes. Coroutines launched in it are not tied to any job and can easily lead to memory leaks and wasted resources. There is almost no good reason to use it in application code.
Job
vs. SupervisorJob
: How the Chef Handles Failure
A CoroutineScope
is defined by its CoroutineContext
, which must include a Job
. The Job
represents the scope’s own lifecycle and how it handles failures.
The default Job
has a strict “all for one, one for all” policy. If any child coroutine fails with an exception, it immediately cancels its parent Job
and all of its siblings.
It’s like a head chef who evacuates the entire section if one cook starts a fire.
|
|
Output:
|
|
Notice “Cook A” never got to finish. Cook B’s failure cancelled the entire scope.
But what if you want one failure to not affect other tasks? For that, you use a SupervisorJob
.
It allows child coroutines to fail independently without bringing down the whole scope
This is the more lenient head chef who deals with the one cook’s mistake while telling everyone else to keep working.
|
|
Output:
|
|
viewModelScope
uses a SupervisorJob
by default, which is why one failing network call in a ViewModel
doesn’t necessarily stop another one from completing.
This makes it incredibly robust for UI-related tasks.
Handling Emergencies: Exception Handling
What happens when a task fails with an error? In the world of coroutines, exceptions propagate up the hierarchy.
An uncaught exception will cancel its parent scope (unless it’s a SupervisorJob
). This is a “fail-fast” safety feature.
To handle errors gracefully without crashing the scope, use a try-catch
block (typically around the .await()
call).
|
|
Output:
|
|
By catching the exception, we handled the problem locally and allowed the operation to finish gracefully.
Wrap-up
Our kitchen is now managed. We have:
Dispatchers
: The special stations that ensure work happens in the right place (Main, IO, Default).CoroutineScope
: The Head Chef who manages the lifecycle of all tasks. It ensures nothing gets leaked.Job
vsSupervisorJob
: Different management styles for handling failure.- Cancellation & Exceptions: Clear protocols for when an order is cancelled or something goes wrong.
What’s Next in Part 3?
So far, our chefs have been preparing one-off orders. The customer asks for one thing, and we await
one result.
What happens when we need a continuous stream of dishes for a tasting menu, or a buffet that needs constant refilling?
In Part 3, we’ll introduce a core concept for handling streams of data: Flow
. We’ll learn how to emit, transform, and collect series of values over time, taking our kitchen’s capabilities to the next level.