Concept of the day · concurrency

Structured Concurrency

A task cannot outlive the scope that started it.

The mechanism. Concurrent tasks are bound to a lexical scope. The scope does not exit until every child task has finished, been cancelled, or failed. Errors propagate to the parent; cancelling the parent cancels all children. Concurrency gets the same shape as a function call: one entry, one exit, no escapees.

What it fixes. Fire-and-forget concurrency loses errors (a rejected promise nobody awaited), leaks tasks (a goroutine blocked on a channel nobody will write to), and makes cancellation impossible, because there is no handle to the work you started three layers down. The classic symptom is a service whose goroutine or task count climbs monotonically until it dies.

Where it exists now. Python's asyncio.TaskGroup (3.11+) and Trio's nurseries, which started this whole line of thinking. Java's StructuredTaskScope (JEP 453, finalised with virtual threads). Swift's async let and task groups. Kotlin's coroutineScope. In Go, errgroup.WithContext is the idiomatic approximation — the language itself lets a bare go statement escape any scope, which is exactly the hole.

The rule that follows. Never start work you have no handle on. If a function starts a task, either it waits for it or it returns something the caller can wait on.

Takeaway. go and asyncio.create_task are the goto of concurrency. Scopes are the structured replacement.

← edition of 2026-11-01cheatsheet