skip to content

In some runtimes, calling an asynchronous function forces the caller to be asynchronous too, splitting an API into two parallel worlds. What causes that split, what problems does it create, and which designs avoid it?

level: principalimportance: should knowfreq 30%

answer

  1. coloring = consequence of the stackless transform
  2. suspendability propagates up every call chain
  3. duplicated APIs; interfaces and callbacks colored too
  4. sync-over-async deadlocks; async-over-sync hides cost
  5. colorless: stackful/user-mode threads or effect systems

basics

~20 s

It comes from the stackless transform: only rewritten frames can pause, so the ability to pause must propagate to every caller. That duplicates APIs, blocks incremental adoption, and tempts unsafe sync-over-async bridging. Stackful coroutines, user-mode threads, and effect systems remove the split.

solid answer

~60 s

The split — often called **function coloring** — is a consequence, not a taste. With stackless coroutines, only compiler-transformed frames hold resume state. An untransformed caller has nowhere to park, so it cannot wait without blocking a thread. Hence "can suspend" propagates up every call chain, and interfaces, callbacks, and higher-order functions each need a suspending variant. Costs: duplicated APIs and near-duplicated implementations; a library cannot go async without a breaking change to every caller; abstractions like map/filter/comparators need colored overloads; and the escape hatches are bad — blocking on an async result invites thread starvation and self-deadlock, while faking async by pushing blocking work onto a pool just moves the cost. Alternatives: **stackful coroutines / user-mode threads**, where any frame can suspend, so there is no color; **effect systems**, which type the capability but let it be polymorphic so one implementation serves both; or **making everything async**, so no sync color exists. The counter-argument: visible suspension markers document exactly where interleaving and cancellation can happen. Colorless suspension hides those points.

go deeper

for a junior

Recall that awaiting requires the caller to also be async, which forces libraries to ship two versions of everything.

for a middle

Explain the mechanism — only transformed frames can hold resume state — and name the classic failure of blocking on an async result inside a bounded pool.

for a senior

Cover ecosystem consequences: colored interfaces and callbacks, duplicated cross-cutting concerns, and why async-over-sync wrappers hide rather than remove blocking cost.

for a principal

Treat it as a platform strategy decision: weigh migration cost of an existing blocking codebase against per-task memory and diagnosability, and note that colorless suspension trades auditable interleaving points for transparency.

## Where the split comes from A stackless coroutine is a compile-time rewrite: the function body becomes a state machine with a heap frame holding its resume point and live locals. Only functions that were rewritten have such a frame. Now consider an ordinary function calling a suspendable one and wanting its result. When the callee suspends, control must return all the way to the scheduler — but the ordinary caller's frame lives on the real stack and would be destroyed. So the caller must also be rewritten. Inductively, the property spreads to the whole call chain up to a scheduler entry point. That is the mechanism. The two "colors" are not a syntax preference; they mark which functions the compiler transformed. ## The costs **API duplication.** Any library offering both worlds carries two surfaces, often with near-identical logic. They drift. **Adoption is a breaking change.** A widely used library cannot make an operation suspendable without recoloring every caller. In practice ecosystems fork into sync and async halves, and each half re-implements HTTP clients, drivers, test harnesses, and instrumentation. **Higher-order abstractions multiply.** A generic collection operation, a retry combinator, a comparator, a resource-scope helper: each needs a suspendable variant, because a callback that can suspend forces its caller to. Generic code that should be color-polymorphic cannot be. **Interfaces are colored.** Once a method on an interface is suspendable, every implementation is — including trivial in-memory or test ones that never wait. Conversely, an interface designed synchronously cannot host a suspendable implementation without redefinition. **Bad escape hatches.** Two appear under deadline pressure: - *Sync over async*: block a thread waiting for an async result. On a bounded pool this can deadlock — the thread you are blocking is one the completion needs to run on — and at minimum it reintroduces the thread-per-wait cost you were avoiding. - *Async over sync*: wrap a blocking call in a task submitted to a pool so it looks suspendable. Nothing became non-blocking; you moved the blocked thread and hid the capacity cost behind an interface that promises cheapness. **Diagnostics and cross-cutting concerns.** Context propagation, tracing, timeouts, and error handling frequently need two implementations, one per color. ## Designs that remove the split **Stackful coroutines / user-mode threads.** Give each task a real stack that the runtime can park and resume. Any frame at any depth can suspend, so an ordinary-looking call can wait without holding an OS thread. Existing blocking-style code becomes scalable without recoloring. Costs: stack memory per task, pointer-stability and pinning problems when foreign frames are on the stack, and it needs runtime/platform support. Also, blocking calls that bypass the runtime (native calls, some file APIs, certain lock primitives) still pin a carrier thread, so the transparency is not absolute. **Effect systems / capability polymorphism.** Type "may suspend" as an effect and allow functions to be generic over it, so one `map` serves suspending and non-suspending callbacks. This keeps the information visible and static without duplicating APIs. The cost is a heavier type system and steeper learning curve; it is mainstream in a few languages and research-grade elsewhere. **Uniform async.** Make everything suspendable; no second color exists. Simple, but you pay transform overhead everywhere and lose the syntactic signal. ## The argument for keeping colors Colors are documentation. A visible suspension marker tells the reader: another task may run here; an invariant must hold here; cancellation can land here; a value read before this point may be stale after it. In colorless models, any call might yield, so reviewers cannot enumerate interleaving points by reading — they are back to preemptive-style reasoning, just without kernel preemption. Colors also make the cost of I/O visible in the type, which some teams value. ## How to answer as a decision Frame it as a portfolio call. For a **greenfield high-density service** in an ecosystem whose libraries are already suspendable, colors cost little and buy auditability. For a **large existing codebase** with deep blocking dependencies you do not control, a colorless model is worth its stack cost because it eliminates a rewrite you would otherwise never finish. For a **library author** in a colored ecosystem, the crucial rule is never to fake either direction: expose the color your implementation actually is, and let the caller choose the bridge with full knowledge of the thread cost.

  • Why can blocking a thread to wait for an asynchronous result deadlock a service?
    The completion that would unblock you has to run on some thread from the same bounded pool, and you are occupying one of them. With enough callers doing this simultaneously, every thread is blocked waiting for completions that have no thread left to run on, so nothing can make progress. The pool is deadlocked even though no lock is involved.
  • Does a colorless model such as user-mode threads make every call non-blocking?
    No. Transparency only extends to operations the runtime knows how to intercept and park. Native calls, some file-system operations, and certain lock primitives still hold the carrier thread, so those tasks are pinned for the duration. You still need to know which operations the runtime can park, which is exactly the knowledge colors made syntactically visible.

Colors are like a passport requirement that propagates: to travel with someone who needs a visa, everyone in the party needs one too. Stackful coroutines are open borders — convenient, but you lose the stamp in your passport that recorded exactly where you crossed.

saying these in an interview costs you the question

  • Calling function coloring a purely aesthetic language-design complaint
  • Recommending blocking on an async result as a routine bridge without mentioning deadlock and capacity risk
  • Claiming a pool-backed wrapper makes a blocking library non-blocking
  • Assuming stackful coroutines make every call non-blocking, ignoring native calls that still pin a carrier thread
  • Missing that colorless models remove the visible markers that show where interleaving and cancellation occur

context