← Back to Blog
Concurrency

Concurrency: why abstractions are not enough

Callbacks, Promises, Fibers, Coroutines reduce complexity but don't eliminate resource leaks and race conditions. Structured Concurrency brings strong guarantees.

📅 ✍️ Antoine Coulon
structured-concurrencypromisesresource-safetyconcurrencyjavascript

Many developers assume that by relying on concurrency abstractions, never touching threads directly, every concurrency problem disappears as if by magic. It’s a comfortable illusion, but an illusion nonetheless. Anyone who has ever written multithreaded code knows just how hard concurrency is to get right. And those who never have can take my word for it.

This second installment continues the first episode, which put thread management into perspective with the Event Loop model. We saw there how a platform can hide the complexity of parallelism behind an event loop. But one essential question remains: once that complexity is concealed, is it actually eliminated?

Abstractions so you never touch threads again

Rather than managing threads by hand, most platforms and languages offer higher-level primitives. The range is wide, and each ecosystem has made its own choices:

All of these abstractions share a single goal: reducing the cognitive load tied to concurrency. They spare the developer from manually orchestrating the creation, synchronization, and destruction of threads. The runtime takes care of it, and that’s precisely what makes them so attractive.

Reducing complexity is not eliminating it

The problem is that reducing complexity is not the same as making it disappear. Even when leaning on these primitives, you remain exposed to a series of very real difficulties:

The verdict is nuanced. Yes, the runtime removes a large class of problems, and managing concurrency becomes significantly simpler. But using these primitives still carries a non-negligible risk, a risk all the more treacherous because it’s invisible behind an API that looks innocent.

A JavaScript example that seems harmless

Take the most mundane case imaginable: running several tasks in parallel with Promise.all.

const result = await Promise.all([
  succeedingTask1,
  failingTask2,
  succeedingTask3,
]);

At first glance, everything looks fine:

And yet, a serious problem is hiding here. When failingTask2 fails (rejection), result is immediately marked as rejected… but succeedingTask1 and succeedingTask3 are not interrupted as a result. They keep running in the background, consuming resources, even though no one cares about their result anymore. This is a resource leak: the program moves on as if everything were done, while two child operations are still running.

Why this behavior? Because there is no relationship between these Promises. Nothing ties the execution of one to that of the others: they are perfectly independent, regardless of the structure of the program that launched them. A Promise gives us easy access to concurrency via the Event Loop, but it carries no notion of hierarchy, ownership, or shared lifecycle.

The Promise here is just an example. The same reasoning applies to most other concurrency primitives: they make it easy to launch parallel operations, but without guaranteeing their coordinated termination.

The solution: Structured Concurrency

This is precisely what Structured Concurrency fills in: a declarative approach that brings strong guarantees where the classic primitives stay silent.

The founding idea is to give a hierarchical context to all of a program’s operations. Each concurrent operation belongs to a parent, and the parent retains control over all of its child operations. If one fails, the others can be interrupted cleanly; if the context is cancelled, everything that flows from it is cancelled too. The lifetime of an operation is thus bounded by the scope that created it, hence the term structured: concurrency follows the structure of the code, exactly as braces delimit the scope of a variable.

Applied to our example, this approach would have had an immediate effect: the failure of failingTask2 would have triggered the automatic cancellation of succeedingTask1 and succeedingTask3, eliminating the resource leak at its root. No orphan operation survives the failure of its peers anymore.

▶ The video demonstration is available on the original LinkedIn post.

Conclusion

Concurrency abstractions (callbacks, Promises, fibers, coroutines) are excellent tools for making parallelism accessible. But they shift complexity rather than remove it: without an explicit relationship between operations, you’re always exposed to resource leaks, race conditions, and sloppy handling of interruptions.

Structured Concurrency answers this gap by imposing a clear hierarchy and termination guarantees. It doesn’t replace the existing primitives: it gives them the structure they were missing. The next time you write a Promise.all, ask yourself what becomes of the tasks you’re no longer waiting on. The answer is rarely the one you’d expect.