Function Coloring at 11: Loom Won, Async/Await Lost

5 min read 1 source clear_take
├── "Async/await is a fundamentally flawed design that infects entire codebases"
│  ├── Bob Nystrom (journal.stuffwithstuff.com) → read

Nystrom's original 2015 essay argues that async functions are 'colored' — they can only be called by other async functions, causing the async designation to virally spread through every caller, interface, and test. He singles out Go, Lua, Ruby, and threaded Java as languages that solved concurrency correctly by not forcing callers to know whether a function might suspend.

│  └── @tosh (Hacker News, 111 pts) → view

By resurfacing the 2015 essay to 111 points on HN, the submitter signals that the function-coloring critique remains relevant — perhaps more so now that a decade of async/await adoption has demonstrated the predicted pain at scale.

├── "Java's virtual threads vindicate the colorless-function approach"
│  └── top10.dev editorial (top10.dev) → read below

The editorial frames Java 21's virtual threads (JEP 444, September 2023) as the most important data point since 2015 — a seven-year effort by Project Loom explicitly designed to make every JVM method effectively colorless. Spring's benchmarks showing Tomcat scaling from ~200 to tens of thousands of concurrent connections without code rewrites are cited as evidence that the runtime, not the programmer, should handle suspension.

└── "The essay reads as prophecy — a decade of evidence now backs the original complaint"
  └── top10.dev editorial (top10.dev) → read below

The editorial argues that eleven years on, the essay 'reads less like a complaint and more like a prophecy.' The industry effectively ran the experiment Nystrom warned about, and the resulting ecosystem of CompletableFuture chains, reactive streams, and Mono<Flux<Either<Error, T>>> type vomit constitutes empirical evidence that function coloring is as harmful as he predicted.

What happened

Bob Nystrom's 2015 essay "What Color is Your Function?" climbed Hacker News again this week, hitting 111 points and generating the usual flood of comments. The essay's thesis, for the three of you who somehow missed it: in languages with `async`/`await`, functions come in two colors. Red (async) functions can only be called from other red functions, or via awkward ceremony — callbacks, promises, `.then()` chains, or the magic `await` keyword. Blue (sync) functions can call other blue functions normally but can't easily call red ones without blocking or spawning. The result is that the color is viral: introduce one async function deep in a library and the red stain spreads through every caller, every interface, every test.

Nystrom wrote the piece in frustration after wrestling with Dart's async story. He singled out Go, Lua, Ruby, and threaded Java as languages that got it right — concurrency primitives that don't force the caller to know whether a function might suspend. Eleven years later, the essay reads less like a complaint and more like a prophecy. The industry spent a decade running the experiment Nystrom warned about, and the results are now ugly enough to count as evidence.

Why it matters

The most important data point since 2015 is Java 21's virtual threads, shipped via JEP 444 in September 2023. Project Loom took roughly seven years from concept to GA, and its design goal was explicit: make every JVM method effectively colorless. A `Thread.sleep()` or blocking JDBC call inside a virtual thread parks the carrier thread's mount, not the OS thread. You write straight-line, synchronous-looking code, and the runtime handles the suspension. No `CompletableFuture` chains. No reactive streams. No `Mono>>` type vomit. The Spring team's benchmarks showed a Tomcat instance going from ~200 concurrent connections (platform threads) to tens of thousands (virtual threads) on the same hardware, with no code rewrite beyond `Executors.newVirtualThreadPerTaskExecutor()`.

Go has had this from day one. Goroutines are colorless: a 4kb stack, scheduled by the runtime, and any function — `time.Sleep`, `http.Get`, a channel send — yields cooperatively without syntactic ceremony. The fact that Go got concurrency right in 2009 and the rest of the industry took fifteen years to catch up is not a flattering data point for the language design community. Kotlin coroutines (stable in 1.3, 2018) chose a middle path: `suspend` functions are colored, but the compiler handles the CPS transform and structured concurrency at least makes cancellation tractable.

Then there's the counterfactual case. Swift adopted async/await in Swift 5.5 (2021). Rust stabilized it in 1.39 (November 2019) and then immediately discovered that `Send`, `Sync`, lifetimes, and `Pin<&mut Self>` made the color problem strictly worse: async Rust isn't just a different color from sync Rust, it's a different ecosystem with parallel libraries (`tokio` vs `std`, `reqwest` vs `ureq`, `sqlx` vs `diesel`). Python's asyncio, retrofitted onto a fundamentally synchronous language, gave us the worst of both worlds — `async def` everywhere, plus `asyncio.run_coroutine_threadsafe` shims when the colors inevitably mix.

The loudest recent signal: Zig ripped async out of the language in 2024. Andrew Kelley shipped 0.11 without async support and stated openly that the team hadn't figured out how to make it work without the coloring problem Nystrom described. "Colorblind async" was Zig's original pitch — a language where the same function could be called sync or async depending on context. They couldn't make it cohere. So they removed it. Removing a major feature in a pre-1.0 systems language is a strong signal that the design space here is harder than it looks, even when you start with the goal of avoiding the trap.

What this means for your stack

If you're on the JVM and still writing reactive code in 2026, you owe yourself a serious look at virtual threads. The migration cost from `CompletableFuture` chains or Project Reactor to plain blocking code on virtual threads is real but bounded, and the readability win is enormous. Spring Boot 3.2+ supports virtual threads out of the box via `spring.threads.virtual.enabled=true`. The main gotcha is `synchronized` blocks, which pin the carrier thread — JEP 491 (targeted for Java 24) fixes this, but until then, audit your code for `synchronized` on I/O paths and replace with `ReentrantLock`.

If you're on Node.js, you're stuck — the entire ecosystem is red, and there's no realistic path to colorlessness short of switching runtimes. Bun and Deno don't fix this; they inherit JavaScript's semantics. Worker threads help for CPU-bound work but don't unify the call graph. The pragmatic move is to accept the color and lean hard on structured concurrency libraries — `effect-ts` and Neverthrow give you at least some discipline over what `await` actually means.

For greenfield Rust services, the conventional wisdom — "use tokio for everything" — is worth questioning. If your workload isn't actually I/O-bound at the tens-of-thousands-of-connections scale, `std::thread` with a thread pool is faster, simpler, and doesn't force the ecosystem split. For a service handling 500 RPS of database-backed requests, the OS thread overhead is rounding error.

For Python, the news is grim. asyncio adoption has plateaued, the ecosystem remains bifurcated (`requests` vs `httpx`, `psycopg2` vs `asyncpg`), and per-PEP-703 progress on removing the GIL is the more interesting concurrency story. If you're starting a new Python service, threads + a thread pool are unsexy but boring-in-the-good-way.

Looking ahead

The long-term direction is clear even if individual ecosystems can't get there. Concurrency belongs in the runtime, not the type system. Goroutines, virtual threads, and Erlang processes all express the same insight: the compiler shouldn't make you decorate every function with a marker indicating whether it might block, because *every function might block*, and the runtime is in a better position than the programmer to handle suspension. The async/await era will be remembered as the decade when an entire industry tried to push that scheduling decision into source code, found it didn't compose, and quietly walked it back via virtual threads. Nystrom's essay keeps resurfacing because it was right, and being right about programming language design eleven years ahead of time is the rarest form of correctness there is.

Hacker News 132 pts 170 comments

What Color is Your Function? (2015)

→ read on Hacker News
rendaw · Hacker News

I really don&#x27;t like this article. It has a catchy, profound-sounding title that people bandy about to argue against stuff they don&#x27;t like.All functions, even non-async functions, are colored. In any large system codebase you&#x27;ll have functions that can only be called in certain situati

preommr · Hacker News

&gt; You still can’t call a function that returns a future from synchronous code. (Well, you can, but if you do, the person who later maintains your code will invent a time machine, travel back in time to the moment that you did this and stab you in the face with a #2 pencil.)Author makes up a lie.T

lukaslalinsky · Hacker News

I&#x27;ve spent the last year working on an async runtime for Zig and I really grew fond of stackful coroutines. Your just program your code as if everything was blocking. The main benefit is that you can use whatever library, it doesn&#x27;t have to be async aware. Heck, I could even use C librarie

nickcw · Hacker News

Go doesn&#x27;t have colored functions due to its nice fat runtime hiding all the async magic away for us.That makes it a pleasure to code concurrent stuff for IMHO.It does have its own similar problems though - does a function return an error? If so you are going to need to plumb the error return t

bre1010 · Hacker News

I wish the key word was instead dontawait and was used inversely to how await is used. 99% of the time I&#x27;m using an async function, despite however slow it is, there&#x27;s nothing for my code to do but wait for it to finish. But if for some reason I would like the next line of code to run befo

// share this

// get daily digest

Top 10 dev stories every morning at 8am UTC. AI-curated. Retro terminal HTML email.