I wish the “Function coloring” meme died. It made sense in the context of the original blog post (which was about callback hell, hence the “4. Red functions are more painful to call” section un the original blog post), but doesn't make sense in the context of async/await. There's literally nothing special with async, it's just an effect among many others.
As soon as you start using function arguments instead of using a global variable, you are coloring your function in the exact same way. Yet I don't think anyone would make the case that we should stop using function arguments and use global variables instead…
Function coloring also only applies to a few select languages. If your runtime allows you can call an async function from a sync function by pausing execution of the current function/thread whenever you're waiting for some async op.
Libraries like Tokio (mentioned in the article) have support for this built-in. Goroutines sidestep the issue completely. C# Tasks are batteries included in that regard. In fact function colors aren't an issue in most languages that have async/await. JavaScript is the odd one out, mostly due to being single-threaded. Can't really be made to work in a clean way in existing JS engines.
“Function coloring” is an imaginary issue in the first place. Or rather it's a real phenomenon, but absolutely not limited to async and people don't seem to care about it at all except when talking about async.
Take Rust: you return `Result<T,E>`, you are coloring your function the same way as you are when using `async`. Same for Option. Errors as return values in Go: again, function coloring.
One of your nested function starts taking a "serverUrl" input parameter instead of reading an environment variable: you've colored your function and you now need to color the entire call stack (taking the url parameter themselves).
All of them are exactly as annoying, as you need to rewrite the entire call stack's function signature to accommodate for the change, but somehow people obsess about async in particular as if it was something special.
It's not special, it's just the reflection that something can either be explicit and require changing many function signatures at once when making a change, or be implicit (with threads, exceptions or global variables) which is less work, but less explicit in the code, and often more brittle.
Async is a Javascript hack that inexplicably got ported to other languages that didn't need it.
The issue arose because Javascript didn't have threads, and processing events from the DOM is naturally event driven. To be fair, it's a rare person who can deal with the concurrency issues threads introduce, but the separate stacks threads provide a huge boon. They allow you to turn event driven code into sequential code.
window.on_keydown(foo);
// Somewhere far away
function foo(char_event) { process_the_character(char_event.key_pressed) };
becomes:
while (char = read())
process_the_character(char);
The latter is easy to read linear sequence of code that keeps all the concerns in one place, the former rapidly becomes a huge entangled mess of event processing functions.
The history of Javascript described in the article is just a series of attempts to replace the horror of event driven code with something that looks like the sequential code found in a normal program. At any step in that sequence, the language could have introduced green threads and the job would have been done. And it would have been done without new syntax and without function colouring. But if you keep refining the original hacks they were using in the early days and don't the somewhat drastic stop of introducing a new concept to solve the problem (separate stacks), you end up where they did - at async and await. Mind you, async and await to create a separate stack of sorts - but it's implemented as a chain objects malloc'ed on the heap instead the much more efficient stack structure.
I can see how the javascript community fell into that trap - it's the boiling frog scenario. But Python? Python already had threads - and had the examples of Go and Erlang to show how well then worked compared to async / await. And as for Rust - that's beyond inexplicable. Rust has green threads in the early days and abandoned them in favour of async / await. Granted the original green thread implementation needed a bit of refinement - making every low level choose between event driven and blocking on every invocation was a mistake. Rust now has a green thread implementation that fixes that mistake, which demonstrates it wasn't that hard to do. Yet they didn't do it at the time.
It sounds like Zig with its pluggable I/O interface finally got it right - they injected I/O as a dependency injected at compile time. No "coloured" async keywords and compiler monomorphises the right code. Every library using I/O only has to be written once - what a novel concept! It's a pity it didn't happen in Rust.
> This was bad enough that Node.js eventually changed unhandled rejections from a warning to a process crash, and browsers added unhandledrejection events. A feature designed to improve error handling managed to create an entirely new class of silent failures that didn’t exist with callbacks.
Zig is just doing vtable-based effect programming. This is the way to go for far more than async, but it also needs aggressive compiler optimization to avoid actual runtime dispatch.
Intuition is relative: when I first encountered unix-style synchronous, threaded IO, I found it awkward and difficult to reason about. I had grown up on the callback-driven classic Mac OS, where you never waited on the results of an IO call because that would freeze the UI; the asynchronous model felt like the normal and straightforward one.
It forces programmers to learn completely different ways of doing things, makes the code harder to understand and reason about, purely in order to get better performance.
Which is exactly the wrong thing for language designers to do. Their goal should be to find better ways to get those performance gains.
> It forces programmers to learn completely different ways of doing things, makes the code harder to understand and reason about, purely in order to get better performance.
Technically, promises/futures already did that in all of the mentioned languages. Async/await helped make it more user friendly, but the complexity was already there long before async/await arrived
If I want sequential execution, I just call functions like in the synchronous case and append .await.
If I want parallel and/or concurrent execution, I spawn futures instead of threads and .await them.
If I want to use locks across await points, I use async locks, anything else?
It is a tool. Some tools make you more productive after you have learned how to use them.
I find it interesting how in software, I repeatedly hear people saying "I should not have to learn, it should all be intuitive". In every other field, it is a given that experts are experts because they learned first.
> I find it interesting how in software, I repeatedly hear people saying "I should not have to learn, it should all be intuitive". In every other field, it is a given that experts are experts because they learned first.
Other fields don't have the same ability to produce unlimited incidental complexity, and therefore not the same need to rein it in. But I don't think there's any field which (as a whole) doesn't value simplicity.
I feel like it's missing my point. Using a chainsaw is harder than using a manual saw, but if you need to cut many trees it's a lot more efficient to first learn how to use the chainsaw.
Now if you take the chainsaw without spending a second thinking about learning to use it, and start using it like a manual saw... no doubt you will find it worse, but that's the wrong way to approach a chainsaw.
And I am not saying that async is "strictly better" than all the alternatives (in many situations the chainsaw is inferior to alternatives). I am saying that it is a tool. In some situations, I find it easier to express what I want with async. In others, I find alternatives better. At the end of the day, I am the professional choosing which tool I use for the job.
I find state machines plus some form of message passing more intuitive than callbacks or any abstraction that is based on callbacks. Maybe I'm just weird.
When I did not know how to program, neither async nor message passing were intuitive. I had to learn, and now those are tools I can use when they make sense.
I never thought "programming languages are a failure, because they are not intuitive to people who don't know how to program".
My point being that I don't judge a tool by how intuitive it is to use when I don't know how to use it. I judge a tool by how useful it is when I know how to use it.
Obviously factoring in the time it took to learn it (if it takes 10 years to master a hammer, probably it's not a good hammer), but if you're fine with programming, state machines and message passing, I doubt that it will take you weeks to understand how async works. Took me less than a few hours to start using them productively.
I would really hate to work with a blue/red function system. I would have to label all my functions and get nothing in return. But, labelling my functions with some useful information that I care about, that can tell me interesting things about the function without me having to read the function itself and all the functions that it calls, I'd consider a win. I happen to care about whether my functions do IO or not, so the async label has been nothing short of a blessing.
Function colouring, deadlocks, silent exception swallowing, &c aren’t introduced by the higher levels, they are present in the earlier techniques too.
I wish the “Function coloring” meme died. It made sense in the context of the original blog post (which was about callback hell, hence the “4. Red functions are more painful to call” section un the original blog post), but doesn't make sense in the context of async/await. There's literally nothing special with async, it's just an effect among many others.
As soon as you start using function arguments instead of using a global variable, you are coloring your function in the exact same way. Yet I don't think anyone would make the case that we should stop using function arguments and use global variables instead…
[dead]
Function coloring also only applies to a few select languages. If your runtime allows you can call an async function from a sync function by pausing execution of the current function/thread whenever you're waiting for some async op.
Libraries like Tokio (mentioned in the article) have support for this built-in. Goroutines sidestep the issue completely. C# Tasks are batteries included in that regard. In fact function colors aren't an issue in most languages that have async/await. JavaScript is the odd one out, mostly due to being single-threaded. Can't really be made to work in a clean way in existing JS engines.
“Function coloring” is an imaginary issue in the first place. Or rather it's a real phenomenon, but absolutely not limited to async and people don't seem to care about it at all except when talking about async.
Take Rust: you return `Result<T,E>`, you are coloring your function the same way as you are when using `async`. Same for Option. Errors as return values in Go: again, function coloring.
One of your nested function starts taking a "serverUrl" input parameter instead of reading an environment variable: you've colored your function and you now need to color the entire call stack (taking the url parameter themselves).
All of them are exactly as annoying, as you need to rewrite the entire call stack's function signature to accommodate for the change, but somehow people obsess about async in particular as if it was something special.
It's not special, it's just the reflection that something can either be explicit and require changing many function signatures at once when making a change, or be implicit (with threads, exceptions or global variables) which is less work, but less explicit in the code, and often more brittle.
[dead]
Async is a Javascript hack that inexplicably got ported to other languages that didn't need it.
The issue arose because Javascript didn't have threads, and processing events from the DOM is naturally event driven. To be fair, it's a rare person who can deal with the concurrency issues threads introduce, but the separate stacks threads provide a huge boon. They allow you to turn event driven code into sequential code.
becomes: The latter is easy to read linear sequence of code that keeps all the concerns in one place, the former rapidly becomes a huge entangled mess of event processing functions.The history of Javascript described in the article is just a series of attempts to replace the horror of event driven code with something that looks like the sequential code found in a normal program. At any step in that sequence, the language could have introduced green threads and the job would have been done. And it would have been done without new syntax and without function colouring. But if you keep refining the original hacks they were using in the early days and don't the somewhat drastic stop of introducing a new concept to solve the problem (separate stacks), you end up where they did - at async and await. Mind you, async and await to create a separate stack of sorts - but it's implemented as a chain objects malloc'ed on the heap instead the much more efficient stack structure.
I can see how the javascript community fell into that trap - it's the boiling frog scenario. But Python? Python already had threads - and had the examples of Go and Erlang to show how well then worked compared to async / await. And as for Rust - that's beyond inexplicable. Rust has green threads in the early days and abandoned them in favour of async / await. Granted the original green thread implementation needed a bit of refinement - making every low level choose between event driven and blocking on every invocation was a mistake. Rust now has a green thread implementation that fixes that mistake, which demonstrates it wasn't that hard to do. Yet they didn't do it at the time.
It sounds like Zig with its pluggable I/O interface finally got it right - they injected I/O as a dependency injected at compile time. No "coloured" async keywords and compiler monomorphises the right code. Every library using I/O only has to be written once - what a novel concept! It's a pity it didn't happen in Rust.
> This was bad enough that Node.js eventually changed unhandled rejections from a warning to a process crash, and browsers added unhandledrejection events. A feature designed to improve error handling managed to create an entirely new class of silent failures that didn’t exist with callbacks.
Java has this too.
Zig is just doing vtable-based effect programming. This is the way to go for far more than async, but it also needs aggressive compiler optimization to avoid actual runtime dispatch.
I like async and await.
I understand that some devs don’t want to learn async programming. It’s unintuitive and hard to learn.
On the other hand I feel like saying “go bloody learn async, it’s awesome and massively rewarding”.
Intuition is relative: when I first encountered unix-style synchronous, threaded IO, I found it awkward and difficult to reason about. I had grown up on the callback-driven classic Mac OS, where you never waited on the results of an IO call because that would freeze the UI; the asynchronous model felt like the normal and straightforward one.
What's awesome or rewarding about it?
It forces programmers to learn completely different ways of doing things, makes the code harder to understand and reason about, purely in order to get better performance.
Which is exactly the wrong thing for language designers to do. Their goal should be to find better ways to get those performance gains.
And the designers of Go and Java did just that.
> It forces programmers to learn completely different ways of doing things, makes the code harder to understand and reason about, purely in order to get better performance.
Technically, promises/futures already did that in all of the mentioned languages. Async/await helped make it more user friendly, but the complexity was already there long before async/await arrived
Yes - I was really talking about "asynchronous programming" in general, not the async/await ways to do it in particular.
What different way of doing things?
If I want sequential execution, I just call functions like in the synchronous case and append .await. If I want parallel and/or concurrent execution, I spawn futures instead of threads and .await them. If I want to use locks across await points, I use async locks, anything else?
> It’s unintuitive and hard to learn.
Funny, because it was supposed to be more intuitive than handling concurrency manually.
It is. A lot.
But concurrency is hard and there's so much you syntax can do about it.
Some come to async from callbacks and others from (green)threads.
If you come from callbacks it is (almost) purely an upgrade, from threads is it more mixed.
Yeah, that's what annoys me, async comes from people who only knew about callbacks and not other forms of inter thread communication.
It is a tool. Some tools make you more productive after you have learned how to use them.
I find it interesting how in software, I repeatedly hear people saying "I should not have to learn, it should all be intuitive". In every other field, it is a given that experts are experts because they learned first.
> I find it interesting how in software, I repeatedly hear people saying "I should not have to learn, it should all be intuitive". In every other field, it is a given that experts are experts because they learned first.
Other fields don't have the same ability to produce unlimited incidental complexity, and therefore not the same need to rein it in. But I don't think there's any field which (as a whole) doesn't value simplicity.
I feel like it's missing my point. Using a chainsaw is harder than using a manual saw, but if you need to cut many trees it's a lot more efficient to first learn how to use the chainsaw.
Now if you take the chainsaw without spending a second thinking about learning to use it, and start using it like a manual saw... no doubt you will find it worse, but that's the wrong way to approach a chainsaw.
And I am not saying that async is "strictly better" than all the alternatives (in many situations the chainsaw is inferior to alternatives). I am saying that it is a tool. In some situations, I find it easier to express what I want with async. In others, I find alternatives better. At the end of the day, I am the professional choosing which tool I use for the job.
Except you're hearing it from someone who doesn't have a problem handling state machines and epoll and manual thread management.
And that was intuitive and easy to learn?
I find state machines plus some form of message passing more intuitive than callbacks or any abstraction that is based on callbacks. Maybe I'm just weird.
When I did not know how to program, neither async nor message passing were intuitive. I had to learn, and now those are tools I can use when they make sense.
I never thought "programming languages are a failure, because they are not intuitive to people who don't know how to program".
My point being that I don't judge a tool by how intuitive it is to use when I don't know how to use it. I judge a tool by how useful it is when I know how to use it.
Obviously factoring in the time it took to learn it (if it takes 10 years to master a hammer, probably it's not a good hammer), but if you're fine with programming, state machines and message passing, I doubt that it will take you weeks to understand how async works. Took me less than a few hours to start using them productively.
It IS intuitive.
After you’ve learned the paradigm and bedded it down with practice.
Frankly, async being non-intuitive does not imply that manual concurrency handling is less so; both are a PITA to do correctly.
I can't follow that it's hard to learn and unintuitive
Surely by section 7 well be talking (or have talked) about effect systems
Wasn’t in the prompt.
I would really hate to work with a blue/red function system. I would have to label all my functions and get nothing in return. But, labelling my functions with some useful information that I care about, that can tell me interesting things about the function without me having to read the function itself and all the functions that it calls, I'd consider a win. I happen to care about whether my functions do IO or not, so the async label has been nothing short of a blessing.
Because all HN needed was another piece of AI slop incorrectly quoting “what color is your function”…
It's 2026 and I'm starting to hate the internet.
[dead]