The Colored Function Problem
In some languages (Python, JavaScript, Rust), async functions and sync functions are different colours. Sync code can't call async without an event loop; async code can't call sync blocking calls without freezing the loop. The colour propagates upward through every caller. Go and Java 21+ virtual threads avoid the problem by making the runtime handle async transparently.
The basic idea
In some languages, "async" functions and "regular" functions are different colours. The rules: an async function can call any function, but a regular function cannot call an async function without an event loop and special bridging. The colour propagates: once any function in the call chain is async, every caller above it has to become async too.
The term is from Bob Nystrom's 2015 essay "What Color is Your Function?".
A picture of the propagation
COLORED LANGUAGE (Python, JavaScript, Rust, C#)
main()
|
v
setupServer()
|
v
handleRequest() -----> must become async handleRequest()
|
v
fetchUser() -----> must become async fetchUser() <-- added an await here
|
v
db.query() -----> async (after switching to an async DB driver)
Adding async at the leaf forces async all the way up the call chain.
Every caller now needs `async def` and `await` at the right spot.
UNCOLORED LANGUAGE (Go, Java 21+)
main()
|
v
setupServer()
|
v
handleRequest()
|
v
fetchUser()
|
v
db.query() <-- parks the goroutine / virtual thread; runtime
handles it. No async keyword anywhere.
In a colored language, switching one leaf function to async ripples through the whole stack. In an uncolored language, the runtime hides the parking inside the function call.
How each side handles it
Colored (Python, JavaScript, Rust, C#) provides async / await syntax. Pros: explicit at every call site whether the call yields. Cons: colour propagates; calling sync blocking code from async freezes the event loop; mixed-colour codebases spend a lot of code on bridges.
Uncolored (Go, Java 21+ virtual threads) hides the parking in the runtime. Pros: code looks like sync; one mental model; "spawn one task per request" is fine. Cons: the call site doesn't reveal whether the call will park; runtime knowledge is required.
Why this matters when picking a language
Python / Node explicit async/await; colour shows up everywhere
frameworks (FastAPI, NestJS) keep most code uniformly async
Go no colour; highly concurrent code looks sequential
goroutines cheap; easy to onboard
Java 21+ virtual threads behave like Go; "thread per request" again
pre-21: callback / CompletableFuture chains, painful
Rust explicit async + borrow checker; very explicit, very safe,
steep learning curve for async
Both designs work. The trade is "explicit at every yield" (colour) vs "syntactically uniform, runtime handles it" (no colour). Pick based on what the team will spend more time on.
How to bridge
When the colour boundary must be crossed in Python or JavaScript:
Sync to async (calling async code from sync): use asyncio.run(coro) to start a fresh event loop, or asyncio.run_coroutine_threadsafe(coro, loop) if a loop is already running on another thread. JS: there's no clean equivalent; sync code can't await, so wrap in .then() callbacks or restructure to async.
Async to sync (calling sync code from async): use asyncio.to_thread(blocking_func, *args) to run the blocking call on a thread pool. The async coroutine awaits the thread completion without freezing the loop. JS: similar with worker_threads for genuinely blocking CPU work.
The Python ecosystem has more bridging tools because it's been mid-transition for years. JavaScript is more uniformly async since Node.js was async from day one.
Picking a language for a new service
If async ergonomics matter to the team, this should be part of the choice:
- Python / Node: explicit async/await. Colour comes up constantly. Frameworks (FastAPI, NestJS) are uniformly async; the codebase lives in async land and the bridging is occasional.
- Go: no colour. Highly concurrent code looks like sequential code. Goroutines are cheap. Easy to onboard.
- Java 21+: virtual threads behave like Go. Pre-21 Java means async-callback land or CompletableFuture chains.
- Rust: explicit async, plus the borrow checker. Very explicit, very safe, very steep learning curve for async.
- Kotlin: colored (
suspend) but with great structured-concurrency primitives.
There's no objectively correct answer. Explicit colour gives precision; transparent runtime gives ergonomics. Choose based on what the team prefers to think about.
The colour problem is real but not catastrophic. Most successful systems have been built in languages with colored functions. The pattern that works in those languages: be uniformly async at the layers that touch I/O, push CPU work to thread pools at the boundary, and accept that the propagation is a fixed cost of the choice.
For a new service where ergonomics matter, Go and Java 21+ remove a whole category of complexity. For teams stuck with Python or Node, learn the bridges and accept the friction.
Implementations
Virtual threads (Java 21+) make blocking calls cheap. The JVM parks the virtual thread during I/O and runs another on the same OS thread. Same code structure as Go: blocking-style code, runtime handles the async-ness.
1 // Looks like blocking code; actually parks the virtual thread on I/O
2 User fetchUser(String id) throws Exception {
3 HttpRequest req = HttpRequest.newBuilder()
4 .uri(URI.create("https://api.example.com/users/" + id))
5 .build();
6 HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
7 return parseUser(resp.body());
8 }
9
10 // Run thousands concurrently with virtual threads:
11 try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
12 for (String id : userIds) {
13 executor.submit(() -> fetchUser(id)); // each gets a virtual thread
14 }
15 }
16
17 // Compare to platform threads pre-Java 21:
18 // Same code, but each thread is ~1MB stack, OS-managed.
19 // Limited to a few thousand threads max.Key points
- •Python / JavaScript / Rust: async is a function colour. Async can call sync; sync cannot directly call async (needs a loop).
- •Once async is added at any layer, every caller above must also be async, or pay an awkward bridge cost.
- •Go avoids it: every function looks blocking; the runtime parks goroutines on I/O. No async/await syntax.
- •Java 21+ virtual threads do the same: write blocking-style code; the JVM provides async-like efficiency.
- •Cross-colour bridging exists (asyncio.to_thread, asyncio.run, asgiref.sync) but it's friction, not a clean solution.
Follow-up questions
▸Why did Python and JavaScript end up with colored functions?
▸Is the colored function problem actually a problem?
▸What about Kotlin coroutines?
▸When would the explicit-colour approach be preferred?
Gotchas
- !Calling an async function in sync code without a loop returns a coroutine/Promise, not a value
- !Calling sync blocking I/O in async code freezes the entire event loop
- !Mixing thread-based code (threading.Lock) with asyncio is the worst of both worlds
- !Adopting async at one layer forces async on every caller above (the 'colouring' propagates)
- !Even Go and Java virtual threads can starve when blocking C extensions don't release the runtime