What is the practical difference between a coroutine launched as a structured child versus one given its own root Job, and why does it matter for lifecycle?
answer
- Structured = in the tree; detached = own root Job
- Detached: no await, no cancel, no failure propagation
- launch(Job()) / CoroutineScope(Job()) / GlobalScope detach
- Detached work outlives its visual scope = leak
- Legit detach = a longer-lived OWNED scope, not ad-hoc Job()
basics
~20 sA structured child is tracked by its parent: the parent waits for it and cancels it. A coroutine given its own fresh Job becomes independent, so the parent neither waits for it nor cancels it — risking leaks.
solid answer
~40 sA structured child inherits the surrounding Job as its parent, so it participates in the tree: the parent stays in Completing until the child finishes, cancelling the parent cancels the child, and the child's failure propagates up. If you instead pass a new Job() (e.g. launch(Job()) or build a CoroutineScope(Job())), the coroutine becomes a root with no parent link to the original scope: the scope won't await it, won't cancel it, and won't see its failures. That is sometimes intentional for genuinely independent background work tied to a longer-lived scope, but accidentally it produces leaked coroutines that outlive their logical owner. The fix is to launch within a properly scoped CoroutineScope whose Job you cancel at the right lifecycle boundary, rather than detaching ad hoc.
code
kotlin · 11 lines// Leaky: detached from the request scope
suspend fun handle() = coroutineScope {
launch(Job()) { slowAudit() } // NOT awaited or cancelled by this scope
} // returns before slowAudit finishes; work leaks
// Better: own it under an app-scoped, cancelable scope
class AppScope {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun audit(block: suspend () -> Unit) = scope.launch { block() }
fun shutdown() = scope.cancel()
}go deeper
Recognizes that some coroutines are tied to a scope and some are not.
Explains that passing a fresh Job() detaches a coroutine so the scope no longer awaits or cancels it.
Weighs leak risk vs intentional independence and prescribes owned long-lived scopes over ad-hoc detachment.
Designs lifecycle ownership boundaries across an app so every coroutine has a clear, cancelable owner and no orphaned work.
## Two shapes **Structured child** — the normal case: ```kotlin scope.launch { // child of scope's Job launch { work() } // grandchild, tracked } ``` Every coroutine is a node in the tree rooted at `scope`'s `Job`. The guarantees apply: parent awaits children, cancel flows down, failure flows up. **Detached / re-rooted** — pass an explicit `Job`: ```kotlin scope.launch(Job()) { work() } // NEW root, not a child of scope val other = CoroutineScope(Job()) // independent scope/root other.launch { work() } ``` Now the coroutine's parent is the `Job()` you supplied, not `scope`'s Job. The link to `scope` is gone. ## What you lose when detached | Aspect | Structured child | Own root Job | |---|---|---| | Parent awaits it | Yes | No | | Cancelling parent cancels it | Yes | No | | Its failure propagates to parent | Yes | No | | Risk of leak | Low | High if unmanaged | A detached coroutine **outlives** the scope it visually sits in. If that scope was tied to a screen, request, or session, the work keeps running after the owner is gone — a classic leak, and on Android a memory leak holding references. ## When detaching is legitimate Sometimes you genuinely want work that survives the current operation — e.g. fire an audit log that should complete even if the request scope ends. The correct pattern is **not** an ad-hoc `Job()`, but launching into a **deliberately longer-lived, owned scope** (e.g. an application-scoped `CoroutineScope` you cancel on shutdown). That keeps it structured under *some* owner instead of orphaned. ## Diagnosing - If `coroutineScope { }` returns but background work keeps running, something inside detached itself. - Grep for `launch(Job())`, `async(Job())`, `CoroutineScope(Job())`, and `GlobalScope` — these are the usual detaching constructs. `GlobalScope` in particular has no lifecycle owner at all. ## Takeaway Detaching trades the safety net of structured concurrency for independence. Do it only when an explicit, owned lifecycle replaces the parent link — never by accident.
- Why is GlobalScope discouraged in this context?GlobalScope has no lifecycle owner, so coroutines launched in it are never automatically cancelled and easily leak. Prefer an owned CoroutineScope you cancel at a real boundary.
- Is detaching ever the right call?Yes, for work that must outlive the current operation (e.g. logging/audit), but only under a deliberately long-lived, owned scope that you cancel on shutdown — not an ad-hoc Job().
A structured child is an employee on the org chart; giving it its own Job() is like a contractor who keeps working even after the project (and its budget) is closed.
saying these in an interview costs you the question
- Reaching for GlobalScope to 'just run something in the background'
- Using launch(Job()) to silence a cancellation without understanding it detaches
- Assuming a detached coroutine is still cancelled with its visual scope
- Not recognizing detached work as a leak source
- Thinking structured vs detached is purely stylistic with no runtime difference