5/26/2026 at 6:13:09 PM
I really don't like this article. It has a catchy, profound-sounding title that people bandy about to argue against stuff they don't like.All functions, even non-async functions, are colored. In any large system codebase you'll have functions that can only be called in certain situations, with the right setup, whatever, and if you're lucky this is communicated by types but regardless those restrictions can't be avoided. It's easy to call low-restriction functions from high-restriction ones and not the other way around.
Furthermore, it's not like the alternative to explicit await doesn't have issues too (that the article doesn't mention). There is inherent complexity, it's a tradeoff, you can't just syntax it away.
by rendaw
5/26/2026 at 7:24:13 PM
What I like least about this article is that it's completely soured the entire context of asynchronous programming. Invariably, any time someone discusses design of an async functionality, function coloring is brought up, with almost no analysis as to how it applies and why it's a good or bad thing. (Ironically, I probably see more in-depth analysis these days as to why this article isn't apropos than why it is when this happens.) It's just reduced to "anything that makes a separation between async and sync is function coloring and that's automatically bad." The existence of any sort of trade-off, or really, the entire meat of the article, is just completely ignored.One thing that can be better called out is that this issue of function coloring isn't just an async problem. Exceptions cause function coloring--and not just Java's controversial checked exceptions. An infallible/fallible domain split is function coloring. Javascript's async handling is called out not because it's doing the function coloring but because--in 2015--the tools that existed for dealing with async code in JS libraries were really, really bad, largely reliant on callback hell. Promises and the async/await keyword fix most of the issues, and the ones that aren't fixed boil down to the fundamental issue that an asynchronous event-loop model and a synchronous batch model are just different programming paradigms to begin with.
by jcranmer
5/27/2026 at 1:15:18 AM
The problem with function color exists when you can't abstract over it[1]Some statically typed languages (I believe both Haskell, ocaml) have powerful type system that allow abstracting over function types and function colors. Color is not an issue here.
Some other statically typed languages (C#, rust, and C++ (at least with the built-in stackless coroutines)) can abstract over types but not over colors. This is a problem.
Some statically typed languages (Go) do not encode async-ness statically, so it is not an issue[2].
Some dynamically typed languages (scheme, Lua, lisp) also do not encode async-ness statically. Everything is fine.
Finally there are some dynamically typed languages (python, js) that, eskew static types but for some reason still decide to encode async-ness statically. For me this is the most bizarre decision, especially as some of the justifications for static async-ness (performance, memory usage) are less relevant.
[1] for example, a litmus test is being able to implement an higher order function that inherits its color from one of its parameters. Essentially this is the problem of generically turning an internal iterator to an external one.
[2] fundamentally in these languages continuations are first class values that can be passed around, so the asyncness is naturally not bound to the function that created a continuation.
Edit: you can in principle abstract away color in any async language by simply assuming that any function call is async and await it. Then sync functions can trivially be made async. But at this point async annotations no longer convey any useful property: the language might as well implicitly await any function and require call-site annotations for diverging control flow or reentrancy requirements. More practically as languages with async evolve and grow asyncness becomes pervasive and pushes away any sync component.
by gpderetta
5/27/2026 at 5:13:40 AM
> The problem with function color exists when you can't abstract over itHopefully it's safe read this as there's no common static type between function and async function meaning APIs (that take functions as arguments) have to provide seperate methods (or overloading) for these different colours.
Like in typescript you can write `<T>(f: () => T) => T` because an async function statically is just the return type wrapped in a Promise, not something like `async () => T` you can still pass in an async function as an argument.
I think that's a reasonable thing to take issue with, and its _possibly_ an avoidable design problem. That said I can see it being less avoidable if the async function requires some special kind of invocation (like being associated with some kind of async runtime and its a compiled language).
When I see people bring the issue of function colouring, the focus tends to be on the fact that a function is no longer interchangeable with a sync function and now you have to handle a promise, which I personally find unconvincing if the return type really should be a promise then it shouldn't be interchangeable with a sync function.
by akst
5/27/2026 at 7:32:14 AM
Your first paragraph links having the colour in the type system as allowing you to write functions that take arguments of parametric colour; your last paragraph says you're unconvinced that you might also like to write functions that return results of parametric colour.An example: a vector of things to a thing of a vector, for "thing" in (promise, option, result<E>, ...). Such a function should only really return a promise if it's given a vector of promises, and, with an interface that "thing" supports, can be written generically for all those things.
(In Rust, there are separate implementations of that for Option and for Future.)
Higher-kinded types are the (a?) design solution, but they _do_ come at a cost, and for some that cost is higher than the cost of colours.
by codebje
5/27/2026 at 12:54:41 PM
I think you're confused, I was talking to two different points, while I'm sure I could have communicated with more precision, either missed it, it was unclear or you don't understand, either way I don't really get the gotcha tone when you could ask for a clarification:Anyways, the two points:
- The first point was, "not having a common way to generalise over both sync, async or blue, green, brown functions, seems avoidable and bad". This is when the type system struggling to common up with a common classification for function invocation independently of colour.
- The second point was that, was "so what if there are different return / wrapping / container / monad types", which focuses on a more common interpretation of this article but a different one.
In Haskell a type in a result, State, Config, Parsec, Maybe is in it for a reason, and thankfully we can generalise over that. Higher kind types (abstracting over abstractions) is a whole other basket, as an ex haskeller I would love to see them more mainstream but admittedly I don't think language authors are convinced and there isn't much we can do about it, so we should learn to make do with what we have outside of haskell.
by akst
5/28/2026 at 1:37:59 AM
I think I was simply not very good at expressing what I was trying to convey, sorry, and it is a fault of mine to come across as gotcha-y even when trying not to. Thank you for responding with patience despite that.The first point I interpret as "colourful arguments are avoidable and bad", with which I agree.
The second point I interpret as "colourful returns are unavoidable but good", with which I disagree - even if that interpretation is too strong and is more "... are unavoidable".
A function's type is its full signature, including inputs and outputs. When you have first-class functions, you have values with function types, and those values are inputs to other functions. Necessarily, then, if you colour outputs you have also applied colour to inputs.
Transposing a vector of things to a thing of vectors is an example of where colourful output forces colourful input. If you cannot abstract over abstractions, you must write and re-write the sequence function for each abstraction.
I'm in agreement with your closing paragraph's sentiment. That HKTs aren't a broadly adopted solution is something I accept, but I reserve the right to low-key begrudge it.
(And the more I write about this, the more I wish the original article had used "flavour" rather than "colour" as I try and probably fail to find phrasing that doesn't simply sound like portions of a racist rant.)
by codebje
5/28/2026 at 5:39:35 AM
> I think I was simply not very good at expressing what I was trying to convey, sorryNo its, and I appreciate you taking the time to read my reply and consider my perspective here.
> The second point I interpret as "colourful returns are unavoidable but good", with which I disagree - even if that interpretation is too strong and is more "... are unavoidable"
Thats fair, but yeah I wouldn't go so far to say its good or imply we should celebrate it in anyways, more so it as a unavoidable constraint that warrants engaging with.
For sure HKT would generalise many stray ends, and there are definately more complicated usecases where you can write much nicer types with HKT, although my experience has been theres been more pain in writing them without HKT than using them without HKT so the pain is a fixed cost of building the library and not an on going problem of using it. Although I am sure there are cases where it's also the case that usage of the library is more painful without HKT.
IDK, I haven't written a ton of Haskell in a while maybe I've forgotten some of its magic and internalised some of the suboptimal aspects of the absense of working without HKT.
But I do from time to time find problems that would be nicer to solve with HKT, I think generally quite a few of them are DSLs or some form of meta programming. I guess in typescript conditional types you can get away with a lot cooked things.
> Transposing a vector of things to a thing of vectors is an example of where colourful output forces colourful input. If you cannot abstract over abstractions, you must write and re-write the sequence function for each abstraction
I've unfortunately also had similar issues from the lack of HKTs with some linear algebra APIs so I don't find this too surprising.
by akst
5/28/2026 at 1:22:19 AM
This is the kind of comment I come to HN for. Thanks for teaching me something.by squirrellous
5/26/2026 at 10:21:39 PM
I am so happy I have never heard anyone IRL say colored functions. It would annoy me. The concept is interesting but like all engineering it is a trade off. In Node amd Go you don't get a choice anyway. In C# you might choose based on performance thinking of thread pools etc IIRC.When programming in Node I find in practice async and "colored functions" no issue especially with async await. Except for performance issues they come with sometimes but not at a programming level.
by dnnddidiej
5/27/2026 at 12:45:07 AM
> When programming in Node I find in practice async and "colored functions" no issue especially with async await. Except for performance issues they come with sometimes but not at a programming level.JS solves this problem in two ways in the ecosystem:
- basically saying "all functions must be async" in practice
- allowing you to await a non-awaitable ("await 3" is valid)
so library authors can "force" async/await, but users don't actually have to interact with it when they don't need to. But "everything" being async/await means it's all 'basically fine' anyways
There's also the fact that JS libraries tend to be "pass in a bunch of callbacks" vs, say, Python's "override this class". It makes it much easier for libraries to have everything be async and have it really not get in the way.
Python libs tend to have much larger API surfaces due to how OOP works. So async-y internals works are harder to isolate cleanly without breaking the public API. But if you make your API "async-first" then the debugging experience in Python is miserable (try pdb'ing your way through awaitables....)
Even here though there are problems. For example, I've tried in the past to replace some lib with a more performant WASM-y thing. But it couldn't be a drop in replacement because the original library was a sync-only API, and the replacement was async!
Something very silly: you write "function add(x, y) { return x+y }". A bunch of people do things like "add(add(x, y), z)" everywhere. You find out you could make "add" "better" with async/await. You now have to get all callers to rewrite.
So what everyone does is just throw _everything_ in to the async/await pile. Which... I guess is fine but I personally dislike writing "await add(await add(x,y)), z)".
(aside: Rust's postfix await at least makes this kinda refactor less annoying)
by rtpg
5/27/2026 at 3:28:55 AM
The Node world was built with asynchronicity in mind. First via callbacks, then Promises, then async/await (Promise-based), so it feels natural now.But if you take Python (for example), it's a shitshow. You usually have two versions of the same API, split by function name, client, package, or namespace: `foo` and `afoo`, where the a-prefixed one is async and meant to be used inside async function call chains, and the other one is the blocking version for non-async chains (which are still very much in use). It's a pain to develop for, to maintain, to scale, everything.
by leonidasv
5/27/2026 at 1:30:45 AM
Actually, it's not async programming. It's only async programming the way JS does it... which is unwinding the entire stack, and then starting another stack on the next tick.Instead, many languages have fibers / coroutines / etc. which simply start new stacks elsewhere, and capture the context.
by EGreg
5/26/2026 at 8:37:01 PM
what I like least about this article is how people seem to just substitute some other, usually theory, notion of what function coloring is to elide the argument (usually to excuse their favorite PL) without actually RTFA. The article is about ergonomics, not PL theory.> Exceptions cause function coloring
do they? Do they?
1) Every function has a color
2) The way you call a function depends on its color
3) You can only call a red function from within another red function
4) Red functions are more painful to call
5) Some core library functions are red
by dnautics
5/26/2026 at 8:48:35 PM
Java's checked exceptions fit the 5 criteria:1. It either `throws` or it doesn't
2. If the function `throws` you have to wrap it in try/catch, or make your function `throws`
3. Your function is `red` if it `throws` the same exception.
4. see (2)
5. See the FileReader class in core.
Now, C++ exceptions might not satisfy all of these, but the problems CheckedExceptions were meant to solve still exist in C++ and as a result some style guides forbid them entirely. Like async, the biggest problem with exceptions were the ergonomics.
by nemothekid
5/26/2026 at 8:59:34 PM
> Like async, the biggest problem with exceptions were the ergonomics.I know it's not a popular take, but I prefer the idea of Checked Exceptions over unchecked ones [0], and suspect current opinions would be vastly different if Java had shipped with some sweet syntactic sugar for: "If an exception that is of kind A or B or C occurs, automatically throw another checked exception X with the original exception as a cause."
> Ex: If I'm writing a tool to try to analyze and recommend music that has to handle multiple different file types, I might catch an MP3 library's Mp3TagCorruptException and wrap it into my own FileFormatException.
This would reduce the temptation for developers to ruin the type-safety characteristics by wrapping everything in a RuntimeException just to get the ticket out the door.
by Terr_
5/27/2026 at 2:42:04 AM
The problem with checked exceptions is that they don't compose with the rest of the type system. Hence the infamous problems with things like Streams. Result types have basically all the virtues of checked exceptions without the problems.by ameliaquining
5/27/2026 at 3:53:45 AM
Result types do have one problem that checked exceptions don’t. Checked exceptions automatically combine into union types in a throws or catch clause. I haven’t seen a language that lets you be generic like that. T fn() throws E, F, G
vs Result<T, E | F | G> // not even Rust lets you do this.
by vips7L
5/27/2026 at 5:36:57 AM
That's more a consequence of Rust needing its tagged unions declared up front so it can lay them out consistently in memory without runtime type information. Python and TypeScript have untagged unions (that are discriminated at runtime by the RTTI attached to all objects in the underlying dynamic language); they don't happen to have an equivalent of Rust's ? operator, but if they did it'd work like you're describing.by ameliaquining
5/27/2026 at 2:03:38 PM
"untagged union" usually means "no discriminant" not "runtime discriminant."(Rust has both tagged (enum) and untagged (union) unions, but untagged ones are unsafe and therefore mostly used for C interop and similar cases.)
by steveklabnik
5/27/2026 at 9:03:32 PM
People often call Python/TypeScript unions "untagged unions" even though they're a very different creature from C/Rust unions. Ideally there'd be a term specifically for them but I'm not aware of one.by ameliaquining
5/27/2026 at 2:04:39 PM
The E | F | G could be two different features, "anonymous sum types" or "union types".TypeScript is an example of a language with union types: https://www.typescriptlang.org/docs/handbook/unions-and-inte...
by steveklabnik
5/26/2026 at 10:00:07 PM
The problem with Java's checked exceptions is that it has too many kinds of exceptions to choose from and they're overly specific. Compare with Go, which has a single error interface and had it from the beginning, so it's used everywhere. Returning a new kind of error is always a local change, unless it's a function that didn't previously report errors at all.Type systems permit either standardization or fragmentation and that's an ecosystem issue. Another example is that a language without a strong consensus on which string type to use will result in a fragmented ecosystem when each library goes its own way.
by skybrian
5/26/2026 at 10:09:57 PM
> too many kinds of exceptions to choose fromI don't understand, why would you need to pick a checked exception? It's the dual or mirror of feeling paralyzed over a return-type because there are "too many kinds of Object to choose from."
If you're writing a CrystalBall class with a gaze_deeply() method, you'll probably return your own VisionResult (extends Object) unless it throws your TooCloudedException (extends Exception).
When someone else writes a wrapper or higher-level layer that uses your code, then it'll be up to them to convert or wrap those results and exceptions into something suitable for their level of abstraction.
> Returning a new kind of error is always a local change
One of my axioms here is that return-values and checked-exceptions are two sides of the same architectural type-system coin. While I'm not familiar with Go, that sounds like something that would be a symptom of bad architecture if it occurred for return values.
In other words, suppose all Java methods always returned Object [0]. That would also ensure that a new return type is "always a local change" to the compiler, but I think most developers would be rightly horrified if they came across code that worked that way.
[0] Let's ignore Java primitives for now.
by Terr_
5/27/2026 at 4:24:24 AM
> you'll probably return your own VisionResult (extends Object) unless it throws your TooCloudedException (extends Exception).> When someone else writes a wrapper or higher-level layer that uses your code, then it'll be up to them to convert or wrap those results and exceptions into something suitable for their level of abstraction.
Why though? What do you gain other than longer stacktraces with all those wrappers? People always trot out some theoretical notion that a caller is going to catch that framework's different exceptions and handle them differently, but have you ever seen calling code that actually did that?
> In other words, suppose all Java methods always returned Object [0]. That would also ensure that a new return type is "always a local change" to the compiler, but I think most developers would be rightly horrified if they came across code that worked that way.
There are many different kinds of values. There really aren't that many different kinds of error - there's "transient error that you might want to retry", "programmer called the API wrong", and that's about it, most other cases (like bad user input) probably shouldn't be exceptions.
by lmm
5/27/2026 at 10:36:23 AM
> Why though? What do you gain other than longer stacktraces with all those wrappers? People always trot out some theoretical notion that a caller is going to catch that framework's different exceptions and handle them differently, but have you ever seen calling code that actually did that?You've never seen a try that has more than 1 catch block for different exception types?
> There are many different kinds of values. There really aren't that many different kinds of error - there's "transient error that you might want to retry", "programmer called the API wrong", and that's about it, most other cases (like bad user input) probably shouldn't be exceptions.
Do you think bad user input should be a result type? Because exceptions are essentially the same thing.
You've hit on a couple of problems with exceptions in Java though. The first is I think the default for checked exceptions should be no stack trace. As the designer of the method you've left it to the caller to decide to handle it or not, if they choose to turn it into an unchecked exception then I believe that is where the stack trace should start from. Assuming there's enough context in the checked exception the designer of the method gave you everything you needed to handle it so why do we need to capture that part of the stack trace? If it ends up getting logged the source of the issue was where it was changed to an unchecked exception.
The other issue comes down to usability. Try catch blocks aren't expressions so if you want to default something in the case of a checked exception it's a lot of low information density lines. Converting to an unchecked exception is also more ceremony than it really needs to be, but there's not really a reason why it couldn't be made simpler with some syntax sugar.
by X0Refraction
5/28/2026 at 12:50:12 AM
> You've never seen a try that has more than 1 catch block for different exception types?I've seen it for APIs that throw exceptions for bad input or whatever. But what I've never seen is more than one catch block for wrapper exceptions (except perhaps to unwrap the cause), where the calling code handles FrameworkNetworkError differently from FrameworkDatabaseError or what have you.
> Do you think bad user input should be a result type?
Yes
> Because exceptions are essentially the same thing.
Well, except for all the ways they're not that you mentioned in the following paragraphs.
by lmm
5/27/2026 at 5:57:14 PM
> Why though?I'm not sure if this means:
1. "Why bother throwing a new exception of a different class, and not bubble up the original as-is?"
2. "Why would you use the standard feature of all Java exceptions which allows you to chain them, and not just throw away the original exception after copying some of its message string?"
For #1, it should be obvious in almost any language, if not instinctive: The library for managing customer records should return `Customer` objects instead `some.database.FetchResult` ones, and likewise it should throw `CustomerAccessException` instead of a `some.database.DatabaseException`. It's a matter of abstraction and preventing weird coupling.
For #2, surely you've been debugging something before and cursed at how the log is missing crucial details that could have saved you hours of trying to reproduce the problem? By chaining exceptions, you get automatic access the inner exception type, message string, additional properties, and deeper stack trace.
* Discarding the 'cause' at this point is a waste, the work was already done, the memory already allocated, why not benefit from it?
* Sometimes it's not your bug, and having the inner exception makes it much easier to get the necessary cooperation of someone else and get it fixed faster.
* Since the declared type is Throwable, it's not creating a compile-time dependency between layers. You could do a softer runtime test on the cause's type, but usually that's a temporary workaround while you complain that someone else's code is hiding critical details.
> there really aren't that many different kinds of error
I feel that's naive, handling edge cases and errors becomes more important as any system gets larger. Some are emergent from the complexity, others always existed but we can't afford to ignore them anymore.
There are many errors because all errors are contextual! Bad-input in the HTML form-submit is not the same as bad-input in the SQL query. A failed invariant of a tree that somehow made a loop is not the same as a failed invariant of something reporting negative length. An SSH handshake error is not a TLS handshake error.
by Terr_
5/28/2026 at 12:55:37 AM
> It's a matter of abstraction and preventing weird coupling.Does that actually work though? Who ever handles CustomerAccessException specifically, except by calling getCause() and looking at the underlying DatabaseException?
> There are many errors because all errors are contextual! Bad-input in the HTML form-submit is not the same as bad-input in the SQL query. A failed invariant of a tree that somehow made a loop is not the same as a failed invariant of something reporting negative length. An SSH handshake error is not a TLS handshake error.
But the way you handle them is the same. All you can ever really do is a) retry it or b) fail it and alert the developer. And the wrapper doesn't make either of those any easier.
by lmm
5/28/2026 at 8:32:27 AM
Note: This subthread has become about exceptions in general, rather than checked-vs-unchecked. I don't mind, but I wanted to make it explicit.> Who ever handles CustomerAccessException specifically, except by calling getCause() and looking at the underlying DatabaseException?
Ideally you call a method like `getDetailFoo()`, and using the inherited `getCause()` is for hacky workarounds, like when you need a change in behavior much sooner than Customer classes will be changed to give you the information you need in a proper way.
As I said before about abstraction and coupling, the programmer here shouldn't need to know that SomeDatabase v.1.2.3 is an implementation detail of Customer. Their code shouldn't need to have a direct dependency on some.database.DatabaseException to compile either. Sure, a Java programmer can use runtime reflection instead... but at that point they should definitely be having second-thoughts about whether they're on a path to the Dark Side.
Regardless of how you write the catch-logic, the chained cause remains important for logging and diagnosis.
> But the way you handle them is the same. All you can ever really do is a) retry it or b) fail it and alert the developer. And the wrapper doesn't make either of those any easier.
I'm going to assume this is equivalent to: "All reactions to errors usually fall into a few broad descriptive categories, and encapsulating the original exception doesn't help the handler make precise decisions."
Does that sound right? Because my initial readings went in much less charitable directions like "the person throwing the exception knows better than you do about how you should handle it later" or "all retries are the same basic logic."
> All you can ever really do is a) retry it or b) fail it and alert the developer. And the wrapper doesn't make either of those any easier.
The wrapper is quite important. Imagine these layers exist:
1. 1-5 different HTTP libraries used by...
2. 5 different API clients for 5 different fortune cookie service, used by...
3. A library that promises a wide and resilient range of fortunes by putting many sources (today, 5) together and semi-randomly picking between them.
4. The website you're making that shows fortunes when people log in.
Do you want layer #1 exceptions to travel all the way to #4 as-is, so that you get an HTTP 500 and you won't actually know which remote service is at fault? (Without grepping the stacktrace, which is *ick*.)No, because either #2 or #3 should be wrapping the HTTP exception in a new one that can carry that extra "which one did I randomly pick" information in a defined way.
Do you want a #2 exception to travel to #4 without changing, so that you have to write catch-clauses (or reflection) for 5 or more exceptions from the 5 different services, code that will be wrong when a version update turns it into 7?
No, because #2's job is to abstract those N services away and throw its own much set of exceptions to cover common cases.
So after all that I realize I didn't specifically address retry-vs-fail choices, but I think this still sufficient to show that layering is necessary.
by Terr_
5/28/2026 at 11:57:11 PM
> Ideally you call a method like `getDetailFoo()`I know that's the theory, but have you ever seen it work that way in practice? Because I haven't.
> Regardless of how you write the catch-logic, the chained cause remains important for logging and diagnosis.
Sure. But if it's not being caught and handled but only logged or investigated then you're not gaining anything from the wrapper.
> Do you want layer #1 exceptions to travel all the way to #4 as-is, so that you get an HTTP 500 and you won't actually know which remote service is at fault? (Without grepping the stacktrace, which is ick.)
Yes? If it's a fail-and-alert-the-developer case then I'm going to look at the stacktrace first anyway.
I might have retries at any layer, but I don't see any case where #4 uses information from a wrapper exception from #3 to make a decision. The only thing #4 is going to do with a failure from #3 is retry or fail, and it's not going to make a different decision based on which service it was. (Maybe you want metrics or circuit breakers on the individual fortune cookie services, but in practice what I've seen is that you'd always find a way to plumb them in at layer #1 or #2, by reflection if need be, rather than have them work of the exceptions coming out of #3. I know the theory you're talking about, I've just never seen it actually work that way)
by lmm
5/27/2026 at 1:03:56 AM
This isn't like returning Object. It's more like returning a String. After using a language with a common String type, who wants to go back to writing code to convert between between different kinds of strings? Having to choose among different string implementations because there's no standard usually leads to boilerplate code doing conversions at the borders.Usually you just want to propagate or log errors, so having a generic error interface is sufficient. It's true that in Java, you can wrap exceptions, but that's extra boilerplate.
(And yes, Go does notoriously have error propagation boilerplate that they should fix, but that isn't a type system problem.)
by skybrian
5/27/2026 at 9:15:45 AM
Exceptions are a very good comparison because they also perform non-local control flow.Checked exceptions are a form of coloring, while unchecked aren't. But Java (which has checked exceptions) has an escape into unchecked land in the form of RuntimeError, most async languages do not, short of spawning a background thread (for sync->async) or force blocking (async->sync).
Interestingly, Result<T,E> based error models are semantically (and even syntactically, mostly, except for the call-site annotation) equivalent to checked exceptions. Usually these languages have enough abstraction capabilities (HKT for example) to make coloring not an issue, or, again, an escape into unchecked land (for example panic in rust or Go, although the latter hardly counts as having Result-like error handling).
by gpderetta
5/26/2026 at 8:58:43 PM
#3 is not satisfied, as you noted in #2. You can call `throws` methods from non-`throws` methods by wrapping the call in a try catch, and `throws` methods can call non-`throws`. There isn't an exclusivity asymmetry like there is for JavaScript async.by sheept
5/26/2026 at 10:57:46 PM
That only applies to Javascript, which, is mostly only red functions anyways (there are no blocking apis in javascript). Javascript doesn't have the coloring problem in the way Python or Rust has it.In Python, you can wrap the call with asyncio.to_thread, in rust with tokio::spawn_blocking.
by nemothekid
5/26/2026 at 11:55:08 PM
I think you got it backwards: JavaScript has the coloring problem while other languages don't."Red functions are more painful to call" alludes to async functions. Every await yields back to the event loop, which adds overhead. Making every function red/async adds a performance cost (and makes it harder to reason about race conditions), which is why JavaScript has a mix of blue and red functions.
Other languages can escape the "red functions can only be called by red functions" trap, like Python asyncio.run or Tokio block_on. JavaScript has no such alternative, not even in Node. Therefore, Python and Rust don't have function coloring, but JavaScript does.
by sheept
5/27/2026 at 3:40:08 AM
I think the big difference is that in an application that cares about throughput and being non-blocking simply blocking isn’t really possible as it affects the whole performance of the application. Wrapping a checked exception in an unchecked one doesn’t do that.by vips7L
5/26/2026 at 8:54:20 PM
Ok, sorry it's been about 20 years since I last javad IIRC you didn't have to declare exceptions in your function signatures. However, wrapping in try/catch seems to violate #3. Try catch is not a heavy lift of a seam between red and blueTo be fair, #3 seems to have shades of grey. In some pls, you can call an async function from a sync one by wrapping it in a whole damn event loop system. Should that count?
by dnautics
5/26/2026 at 9:03:28 PM
> Ok, sorry it's been about 20 years since I last javad and you didn't have to declare exceptions in your function signatures.You're probably remembering RuntimeExceptions, which are a subgroup [0] that are exempt from "checking" by the compiler, which means it does not require method signatures to declare "I might emit this."
[0] https://docs.oracle.com/en/java/javase/26/docs/api/java.base...
by Terr_
5/27/2026 at 3:43:31 AM
You can declare your runtime exceptions too. The compiler won’t enforce you to catch them though. void fn() throws IllegalStateException
by vips7L
5/26/2026 at 11:05:29 PM
Your question just kicks the can down the road. The problem with the article, to me, is the author doesn't want to accept a certain amount of complexity and has erected arbitrary road blocks.To you, a "whole damn event loop system" is too high a price to pay, but try/catch is not. The complexity of exceptions is invisible to you. However there are certain environments (e.g. FFI) where I dont want "the whole damn exception runtime".
by nemothekid
5/27/2026 at 3:58:54 AM
Maybe author do not want to hide the complexity in functions?by melagonster
5/27/2026 at 1:15:59 AM
i mean no? the coloring problem appears to be solvable in zig. i maintain an FFI binding library for the BEAM vm, where once zig finishes its stackless coroutine support, the same zig function written once should be fully interchangeable between non-async, threaded-async, or async-with-yieldpoints-wrapped-in-the-BEAM's-scheduler.by dnautics
5/27/2026 at 4:27:48 AM
> Try catch is not a heavy lift of a seam between red and blue> To be fair, #3 seems to have shades of grey. In some pls, you can call an async function from a sync one by wrapping it in a whole damn event loop system. Should that count?
I think you have to count any extra overhead where you can't just write f(), including try/catch. It's always possible to call whatever kind of function from whatever other kind of function if you put enough effort and hackery in, so if we can't use functions of kind x in functions of kind y as normal "f()" function calls then that has to be what we mean by colouring.
by lmm
5/26/2026 at 9:02:12 PM
Let's go through the list:> 1) Every function has a color
Every function either throws an exception to indicate failure or doesn't. There's actually several different function colors available here, based on how failure is indicated: throwing exception, aborting the process, composite return value, error code return value, global errno-like variable, error code as a parameter, ....
> 2) The way you call a function depends on its color
See above.
> 3) You can only call a red function from within another red function
Some of the failure methods, like aborting on failure, cannot be converted to another mode at all (or only with very great difficulty). Others, like exceptions and errno-based routines, come with environmental constraints that could be contained by an error conversion routine in theory but may be precluded due to how the system as a whole works (e.g., a global variable errno doesn't play well with threads). Which isn't quite the same thing, but then again, "red function" here is async function, and the call-async-from-sync variant is the easier one to pull off (you spin the event loop), and has roughly the same issues as trying to box an exception routine: it only works if the system as a whole has mechanisms to make it work.
> 4) Red functions are more painful to call
Okay, you've got me here... the exception routines are the easier ones to call, syntactically than non-exception-based ones. Internally in the optimizer, however, exceptions are definitely the worst form (even errno somehow ends up working out better, and that's also deeply problematic).
> 5) Some core library functions are red
Oh yes, standard libraries love using a mix of all of these error-handling routines. Look up C++ <filesystem> for example.
by jcranmer
5/26/2026 at 10:12:28 PM
Failing is a color, but throwing an exception isn't. An exception-throwing computation can easily embedded in a computation that doesn't throw - you can catch and return null, etc. But very rarely can a computation that may fail be part of a computation that may not fail.by tardedmeme
5/26/2026 at 8:25:59 PM
this article seems like a nice view into what things were like in 2015 but we've come along way since then.by smcg
5/26/2026 at 10:10:08 PM
What I like most about the article is that it drove the conversation to realising that async is just a poor reimplementation of threads, and put the focus back on how to do threads faster.by tardedmeme
5/26/2026 at 10:23:24 PM
Probably you are right except for some pathological scenarios. Threads and green threads and models where you have have 10000s of threads and not even hit the cardio fat burning zone.by dnnddidiej
5/26/2026 at 6:33:40 PM
Yep. It's not an async vs not async thing. The way some people talk about it, you'd think the async keyword was at fault. It's all about whether a function is callable in some context.Passing in the context as an argument or making it a global variable or returning a monad doesn't do anything to uncolor the function. What's the difference between `async function f()` and `function f(eventloop, callback)`? Only syntax.
Not to mention there's lots of colors unrelated to async, that most languages don't type at all. And if you use the wrong one, your program just doesn't work correctly at runtime. Thread-safe vs thread-unsafe. Blocking vs non-blocking. May throw/panic vs won't throw/panic. May fail/return null vs infallible.
by hmry
5/26/2026 at 7:39:10 PM
> Passing in the context as an argument or making it a global variable or returning a monad doesn't do anything to uncolor the function. What's the difference between `async function f()` and `function f(eventloop, callback)`? Only syntax."Only syntax" is assuming, mistakenly, that syntax doesn't matter.
Also there is a big semantic difference there.. that being in one case you have the flexibility of the passed in parameters taking different forms vs. the static 'async' statement.
It is not strictly an async thing, but a general rule that additional keywords are less powerful than parameters in all cases. Ask any Lisp developer what the difference is..
by eikenberry
5/27/2026 at 1:57:58 AM
Ante has some points on this issue: https://antelang.org/blog/why_effects. All of this is just different syntax in other languages and solved but the abstraction provided seems to be neater.by epestr
5/26/2026 at 7:21:22 PM
I believe a clearer example would be: `async function f(): Foo` vs `function f(): Future<Foo>`. Isn't it how it works inside anyway?by deepsun
5/26/2026 at 8:44:07 PM
> What's the difference between `async function f()` and `function f(eventloop, callback)`? Only syntax.Negative.
what is the "async prefix" equivalent of the following?
global e: eventloop;
noasync fn parent()
childfn(e)
end
by dnautics
5/26/2026 at 9:15:42 PM
It's an interesting repeat submission to study how HN comments change over time though.Regarding content, I agree with you. Async/Await is an amazing paradigm in JS for simplifying callback patterns and non-blocking suspense.
In other programming languages, there exist other intriguing paradigms that are more elegant and emphasize other aspects of "async"; my prime example 2 would be Erlang, but I am not experienced in, for example, Rust or C#.
The article has the same properties that many successful people IRL have: it makes a certain ick very easy to feel and understand, but it doesn't offer much in terms of profound knowledge.
What it does offer though is a perfect spark of discussion, making people who, for example, only know the single-threaded async-await from JS, consider the sheer possibility of other approaches. I am among those people with a limited horizon, presupposing that "knowing" means deep experience to you.
I have some superficial experience with Java physical threads, also with C#, but $job uses JS/TS.
And even in JS, none of this is trivial in my mind.
Consider the deceptively simple question of a kind of "mutex" that enables an async function or method to control concurrency of its own invocation.
The answer to this simple question (queueing promises and clean rejection handling) is already far from trivial, involves the microtask queue, and shows where the mental model of JS-async-await begins to deteriorate.
by moritzwarhier
5/26/2026 at 11:33:54 PM
While non-trivial in several ways, there are standard Web APIs such as Web Workers, Web Audio API, OffscreenCanvas, SharedArrayBuffer, etc. which help to construct modern, multi-threaded applications in JavaScript. Hopefully today, any experienced JavaScript-focused web developer should be experienced with more async paradigms than just async/await.I think these APIs address real issues, but it also makes the entire stack more complex when integrated language support might be better for some features. But, keeping things separate does mean JavaScript as a language is fairly portable and backwards-compatible, if you ignore Web API support. Still, a lot of other languages feel more batteries-included and have more elegant multi-threaded async fundamentals.
by soulofmischief
5/27/2026 at 6:49:51 PM
Yes, my horizon is not quite that limited, but in fact I mostly get to use these capabilities you mention in hobby projects, apart from a few exceptions, given the stack I currently work on for a living. I had Worker threads in mind when I wrote the comment but considered it a disgression.Didn't want to say that JS cannot provide other paradigms for asynchronous code apart from the async function syntax and Promises.
by moritzwarhier
5/26/2026 at 9:28:47 PM
> It's an interesting repeat submission to study how HN comments change over time though.We've had at least a decade of using these async/await languages and discovered function colouring isn't a problem.
by slopinthebag
5/26/2026 at 7:49:04 PM
I'm fairly sure the author was making reference to the famous article What Color Are Your Bits? https://ansuz.sooke.bc.ca/entry/23 which is even more abstract, because some large numbers are coloured "legal" and the same large numbers are coloured "illegal", based on where you got the numbers from.All functions are not coloured, don't try to wriggle out of it by generalising. This article is a specific complaint about Javascript. Javascript is a hack on top of a hack. Its async/await is crap. Javascript requires this "colouring" in a way that C#, Java, Go, Python, Ruby, C, C++, Rust, etc. don't, because they don't have to pretend they're a single-threaded event processing loop, while Javascript does.
by amiga386
5/26/2026 at 8:42:05 PM
I haven't used python recently, but in the days of asyncio it was very much "painful" (to borrow the article's verbage) to use, precisely because of the five criteria in the article.by dnautics
5/26/2026 at 9:02:09 PM
Painfulness isn't the main issue with colored functions, it only explains why we don't make every function red (async).The main issue is that sync functions can't call async functions, but in Python, you can bypass that restriction with asyncio.run.
by sheept
5/26/2026 at 9:07:48 PM
Fta4) Red functions are more painful to call
I guess for me if I reach back to my memory my real problem with asyncio was that it used decorators and wrapping my head around how it was a crazy abuse of generators, completely broke my internal model of how python works (and also how at the time debugging became problematic), and maybe not so much the ergonomics, so strictly speaking a different set of ergonomic problems than in the colored function article
by dnautics
5/26/2026 at 10:55:49 PM
Agreed. Every time this article makes the rounds, people get hung up on the syntax, and not the performance considerations discussed towards the end. Nystrom wrote a whole book on interpreters, and his main criticism here is the big mess of chained closures this makes at runtime, compared with pausing/resuming green threads.by pverheggen
5/27/2026 at 1:37:07 AM
Something I have been thinking about recently is this: metaphors are often a way that authors use to make an argument in a way that is more engaging than: here is fact A, here is fact B, etc. But some metaphors are so strong that they make a stronger argument than the actual facts! And when you hear an argument with such a strong metaphor, you can often end up feeling very convinced of a particular point, even though the point itself isn't worth feeling so convicted over! I feel a very strong metaphor hijacks the emotional part of your brain before the rational part of your brain can figure out if it actually makes sense or not.In the case of this article, the metaphor is extremely strong. Colored functions! What could be dumber! You feel like you see the world clearly after reading the article, and you pity the people who can't see the clear categorization of functions like you can! But, in reality, when has this ever annoyed me? Never! I've worked with async functions in JS/TS for over a decade and there hasn't been a single time in my entire life where I've been frustrated or backed into a corner because I wanted to call an async function from a sync one. At worst I've had to tack on "async" to a couple function declarations and be on with my day.
by johnfn
5/27/2026 at 3:26:29 AM
I do find it annoying. Let's say in JS I have `result = list.map(f)` but now `f` returns a Promise.`result = await Promise.all(list.map(f))` is less pleasant to read. And before writing it, I have to think if I want the `f` function to execute concurrently across all entries of the list, or one at a time: `for (const elem of list) { await f2(elem) }`.
Or maybe I should use a library like `p-map` and carefully set the concurrency level. Or maybe I should create a bulk version of `f` that takes an array and is more efficient than calling `f` N times.
And don't get me started when there's `list.forEach(f)` and `f` becomes async, so now it executes concurrently for all elements, and the engineer who made the change didn't realize it.
And then there's Async Generators ...
by sqquima
5/27/2026 at 4:30:12 AM
> I have to think if I want the `f` function to execute concurrently across all entries of the list, or one at a time: `for (const elem of list) { await f2(elem) }`.I'd consider that a positive rather than a negative. That's an important question to think about (usually) and I want the type system to help remind me.
by lmm
5/27/2026 at 4:42:07 PM
But these are all legitimate choices you have to make, each with their own tradeoffs. Fire-and-forget, all in parallel, and batch are all different - you might end up selecting any based on the characteristics of the work you have to do.by johnfn
5/26/2026 at 6:31:32 PM
Every time this is posted, it’s worth reminding: async functions in JavaScript are the correct design, and the people who did it deserve praise.by kubb
5/26/2026 at 7:36:44 PM
C# (and F# before it) got it (mostly) right. JS did a shallow copy of it, messing up some details, making it harder to use for certain things.by tikotus
5/26/2026 at 6:49:38 PM
Making await flatten promises was kind of questionable. But that’s my main beef.by moomin
5/27/2026 at 4:31:24 AM
Async functions are an ad hoc, informally-specified, bug-ridden, slow implementation of half of monad support.by lmm
5/27/2026 at 12:37:39 AM
> It's easy to call low-restriction functions from high-restriction ones and not the other way around.do you have an example in mind when you say this? I think there's some unique messes with async/await (especially when combined with OOP and extension points... either your extension points _all_ have to be async or you have to have awkward restrictions), in a way that, say, permission checks don't have IMO.
a syntax-less "await function" that climbs up the stack trace to whoever is waiting and holds onto the suspension _is_ a way out of the problem the article describes. Requires runtime support but it effectively means you can always suspend.
by rtpg
5/27/2026 at 12:34:54 AM
> All functions, even non-async functions, are colored.Not by the analogy laid out in the article.
by jmull
5/27/2026 at 2:01:58 AM
I agree it is a dumb article.It made a big fuzz back then but to me it's excellent as a litmus test. Learning async grammar in Javascript takes you an afternoon, after a week it should become familiar. If someone is unable to grasp that ... well, that tells you where they stand.
by moralestapia
5/27/2026 at 8:59:55 AM
> I agree it is a dumb article.It is a great article. Just because you misinterpret it doesn’t make it dumb.
by wiseowise