Structured Concurrency
Structured concurrency ties child tasks to the lexical scope of their parent, so cancelation, errors, and completion propagate through a task tree instead of leaking orphans.
Definition
Structured concurrency treats concurrent operations as scoped constructs: a parent scope starts child tasks, and the scope does not complete until all children finish, succeed, or cancel. Errors fail the scope and cancel siblings; lifetime is the block, not the heap. It eliminates orphaned tasks, leaked timers, and forgotten await points.
Context and scope
Implemented as asyncio.TaskGroup, Kotlin coroutines with coroutineScope, Java StructuredTaskScope, and effect-based async scopes in TS. The counter-pattern is fire-and-forget promises.
Common misconceptions
- It is just Promise.all — Promise.all awaits results, but unawaited rejection paths and no child cancellation make it weaker than a scope with cancel propagation
- Background work needs escape from the structure — detached tasks are sometimes right; the point is that escaping is explicit and auditable
- Structured concurrency requires a specific language — it is a discipline libraries implement in most async ecosystems
Anti-patterns
- Fire-and-forget promises inside request handlers and leaking tasks across requests on retry storms
- Starting children after scope exit or capturing the scope to spawn later, breaking the lifetime invariant
- Swallowing scope cancellation exceptions and turning cooperative cancellation into silent data corruption
Related concepts
Related examples
Authoritative source
What it is
Blocks scope resources; concurrency historically did not scope tasks. A detached promise is a resource with no owner: no one cancels it, no one observes its error, no one bounds its lifetime. Structured concurrency gives tasks the same lifetime rules as local variables.
How it works
A scope opens, child tasks register, and the join point enforces completion semantics: wait-for-all, wait-first-and-cancel-rest, or gather results. A failing child marks the scope failed and requests cancellation of siblings; the scope rethrows the representative error at the join point. Exit guarantees no task outlives the block.
When to use it
Request handlers fanning out to multiple services, batch jobs with early-exit conditions, and any place partial failure should cancel the rest. Pair with cancellation tokens or cooperative yield so children actually respond to scope cancellation.
Pitfalls
Blocking inside a child that ignores cancellation turns structured discipline into a hang. Legacy APIs that self-manage lifetimes — timers, watchers — need adapters to register with the scope. Escape hatches exist for genuine background work but must be visible in review.