11 ms·
What async promised and what it delivered
- cdaringe 6mo agoSurely by section 7 well be talking (or have talked) about effect systems
- twoodfin 6mo agoWasn’t in the prompt.
- andrewstuart 6mo agoI 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”.
- nottorp 6mo ago> It’s unintuitive and hard to learn. Funny, because it was supposed to be more intuitive than handling concurrency manually.
- palata 6mo agoIt 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.
- nottorp 6mo agoExcept you're hearing it from someone who doesn't have a problem handling state machines and epoll and manual thread management.
- palata 6mo agoAnd that was intuitive and easy to learn?
- nottorp 6mo agoI 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.
- palata 6mo agoWhen 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.
- dullcrisp 6mo agoRight but how do you expose your state machine and epoll logic to callers? As a blocking function? As a function that accepts continuations and runs on its own thread? Or with no interface such that anyone who wants to interoperate with you has to modify your state machine?
- brazzy 6mo ago> 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.
- palata 6mo agoI 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.
- littlestymaar 6mo agoIt is. A lot. But concurrency is hard and there's so much you syntax can do about it.
- andrewstuart 6mo agoIt IS intuitive. After you’ve learned the paradigm and bedded it down with practice.
- afiori 6mo agoSome 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.
- nottorp 6mo agoYeah, that's what annoys me, async comes from people who only knew about callbacks and not other forms of inter thread communication.
- josephg 6mo agoNot true. I’ve used both, and I often prefer the explicitness of async await. It’s easier to reason about. The language guarantees that functions which aren’t async can’t be preempted - and that makes a lot of code much easier to write because you don’t need mutexes, atonics and semaphores everywhere. And that in turn often dramatically improves performance. At least in JS. I don’t find async in rust anywhere near as nice to use. But that’s a separate conversation.
- shakow 6mo agoFrankly, async being non-intuitive does not imply that manual concurrency handling is less so; both are a PITA to do correctly.
- tcfhgj 6mo agoI can't follow that it's hard to learn and unintuitive
- brazzy 6mo agoWhat'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.
- swiftcoder 6mo ago> 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
- brazzy 6mo agoYes - I was really talking about "asynchronous programming" in general, not the async/await ways to do it in particular.
- tcfhgj 6mo agoWhat 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?
- marssaxman 6mo agoIntuition 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.
- cmrdporcupine 6mo agoOr... we've learned it and don't like it? For legitimate reasons?
- jandrewrogers 6mo agoIt is an intrinsic tradeoff. With async there is significantly more code complexity with substantially higher performance and scalability. If you don't need the performance and scalability then it is not unreasonable to argue that async isn't worth the engineering effort.
- Yokohiii 6mo agoReally? async/await is the model that makes it really easy to ignore all the subtleties of asynchronous code and just go with it. You just need to trial and error where/when to put async/await keywords. It's not hard to learn. Just effort. If something goes wrong, then "that's just how things go these days".
- deleted 6mo ago[deleted]
- joelwilliamson 6mo agoFunction colouring, deadlocks, silent exception swallowing, &c aren’t introduced by the higher levels, they are present in the earlier techniques too.
- chmod775 6mo agoFunction 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.
- littlestymaar 6mo ago“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.
- tayo42 6mo agoYou can still use a function that returns result in a function that uses option. And result and option usually mean something else. Option is a value or none. None doesn't necessarily means the function failed. Result is the value or an error message. You can have result<option, error> That's different then async where you can call the other type.
- paulddraper 6mo ago> 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.
- littlestymaar 6mo agoBecause 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.
- twoodfin 6mo agoAnd it even got re-upped on the second-chance queue despite plenty of engagement a few days ago!
- nrds 6mo agoZig 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.
- charlieflowers 6mo agoCan you monomorphize the injected effect handlers using comptime, for io and allocators (and potentially more)?
- F-W-M 6mo agoI know what a vtable is, but what is vtable-based effect programming?
- nrds 6mo agoWell... effect programming using vtables. I think this is an emerging paradigm, but it is very early yet so it's difficult to define precisely. My primary inspiration for the concept is theorem proving languages like Lean in which typeclasses ("interfaces" in the OOP terminology) are implemented using structures passed down as arguments ("vtables" in the OOP terminology) separately from any receivers (values of the type implementing the interface, which doesn't actually need to exist for Lean). Typeclasses (and interfaces) are an effect, albeit a simple and limited one. Lean can't express effects in their generality due to totality requirements, but the same mechanism would work perfectly well for effects too. As for the "vtable" aspect: the primary distinction in implementing typeclasses using exposed vtable passing is that the language does not in any way limit the programmer to zero or one implementations of a typeclass per receiver type(s) (cf. orphan rules in Rust, cf. compiling effect systems to witness-passing, etc.).
- edmondx 6mo ago[dead]
- wesselbindt 6mo agoI 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.
- rstuart4133 6mo agoAsync 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.
- 01HNNWZ0MV43FF 6mo agoWhat if process_the_character takes multiple seconds waiting on a network request?
- mbid 6mo agoHow many systems are there that can't just spawn a thread for each task they have to work on concurrently? This has to be a system that is A) CPU or memory bound (since async doesn't make disk or network IO faster) and B) must work on ~tens of thousands of tasks concurrently, i.e. can't just queue up tasks and work on only a small number concurrently. The only meaningful example I can come up with are load balancers, embedded software and perhaps something like browsers. But e.g. an application server implementing a REST API that needs to talk to a database anyway to answer each request doesn't really qualify, since the database connection and the work the database itself does are likely much more resource intensive than the overhead of a thread.
- ozgrakkurt 6mo agoAsync does make nvme io faster because queueing multiple operations on the nvme itself is faster.
- mbid 6mo agoThis is outside of my expertise, but wouldn't multiple threads each submitting a single operation in parallel have the same effect?
- ozgrakkurt 6mo agoThat is still “async” considering what gp wrote. Because they wrote “thread per task” which I assume to mean something like “each os thread handles the work submitted by one user”. This is beside the point but, something like io_uring is still significantly better than doing threadpool nvme io.
- anonymars 6mo agoI'm not sure this is correct mental model of what async solves Async precisely improves disk/network I/O-bound applications because synchronous code has to waste a whole thread sitting around waiting for an I/O response (each with its own stack memory and scheduler overhead), and in something like an application server there will be many incoming requests doing so in parallel. Cancellation is also easier with async CPU-bound code would not benefit because the CPU is already busy, and async adds overhead See e.g. https://learn.microsoft.com/en-us/aspnet/web-forms/overview/performance-and-caching/using-asynchronous-methods-in-aspnet-45#:~:text=On%20the%20web,the%20server%20hardware https://learn.microsoft.com/en-us/aspnet/web-forms/overview/... and https://learn.microsoft.com/en-us/aspnet/web-forms/overview/performance-and-caching/using-asynchronous-methods-in-aspnet-45#:~:text=In%20general%2C%20use%20asynchronous,service%20request%20to%20complete https://learn.microsoft.com/en-us/aspnet/web-forms/overview/...
- fl0ki 6mo agoAsync ruined Rust for me, even though I write exactly the kind of highly concurrent servers to which it's supposed to be perfectly suited. It degrades API surfaces to the worst case :Send+Sync+'static because APIs have to be prepared to run on multithreaded executors, and this infects your other Rust types and APIs because each of these async edges is effectively a black hole for the borrow checker. Don't get me started on how you need to move "blocking" work to separate thread pools, including any work that has the potential to take some CPU time, not even necessarily IO. I get it, but it's another significant papercut, and your tail latency can be destroyed if you missed even one CPU-bound algorithm. These may have been the right choices for Rust specifically, but they impair quality of life way too much in the course of normal work. A few years ago, I had hope this would all trend down, but instead it seems to have asymptoted to a miserable plateau.
- bluegatty 6mo agoIs there hope that green threads could solve this?
- convolvatron 6mo agothis is dead true to me. I write systems code. Rust is supposed to be a systems language. Because I do work that is effectively always written as if it's in the kernel mode and distributed over the network, everything I do is async by default. And the ergonomics around async are just miserable, littered with half-finished implementations and definitions (i.e. async streams, async traits), and a motley bunch of libraries that are written to use particular modes of tokio that don't necessary play well together. its a giant bodge that would be excusable if that wasn't supposed to be part of the core applicability of the language. not to mention that the whole borrower business becomes largely useless, so you forgot to add Arc+Mutex, and Pin implicitly to your list of wrapper type signatures. what bother me the most, is that aside from async, I _really_ do like the language and appreciate what its trying to do. otherwise I would just turn away from the whole mess. this just landed really badly.
- josephg 6mo agoI completely agree. I really like rust, but all the async stuff is so half baked. It’s shocking coming from the JavaScript ecosystem. Async feels - comparatively - incredibly simple in JS. Even async streams are simple in JS and they work great. And I don’t have to wait 10 years for the linker to process all of tokio for a 1 line change.
- SebastianKra 6mo agoThe discussion around async await always focuses on asynchronous use-cases, but I see the biggest benefits when writing synchronous code. In JS, not having await in front of a statement means that nothing will interfere with your computation. This simplifies access to shared state without race conditions. The other advantage is a rough classification in the type system. Not marking a function as async means that the author believes it can be run in a reasonable amount of time and is safe to run eg. on a UI main thread. In that sense, the propagation through the call hierarchy is a feature, not a bug. I can see that maintaining multiple versions of a function is annoying for library authors, but on the other hand, functions like fs.readSync shouldn’t even exist. Other code could be running on this thread, so it's not acceptable to just freeze it arbitrarily.
- bironran 6mo agoIt’s a slop alright. But it also missed the next mainstream iteration which is Java virtual threads / Goroutines. Those do away with coloring by attacking the root of the problem: that OS threads are expensive. Sure, it comes with its own issues like large stacks (10k copy of nearly the same stack?) and I predict a memory coloring in the future (stack variables or even whole frames that can opt out from being copied across virtual threads).
- dcan 6mo agoI will agree - async rust on an operating system isn’t all that impressive - it’s a lot easier to just have well defined tasks and manually spawn threads to do the work. However, in embedded rust async functions are amazing! Combine it with a scheduler like rtic or embassy, and now hardware abstractions are completely taken care of. Serial port? Just two layers of abstraction and you have a DMA system that shoves bytes out UART as fast as you can create them. And your terminal thread will only occupy as much time as it needs to generate the bytes and spit them out, no spin locking or waiting for a status register to report ready.
- cmrdporcupine 6mo agoDespite my panning of async elsewhere on this thread, I agree with you here. Embassy is a thing of beauty and a great use of Rust's async. Much of my embedded career was bogged down managing a pile of state machines. With async/await and embassy, that just goes away.
- likeabbas 6mo agoyou’re talking to the core of the issue. In no other language did they try to satisfy the case of running on an embedded system vs general purpose computing. Async rust tried to, and came up with a solution that is not great for the majority of programmers writing rust. I wish to God that the rust library devs would admit to this fact - say that async rust should stay for embedded runtimes usecases, but we shouldn’t be forcing async across the majority of general purpose computing libraries. It’s just not a pleasant experience to write nor read. And it really doesn’t give any performance benefits.
- mplanchard 6mo agoI write reams of async rust for a living, and completely disagree with this characterization. The concurrency primitives in the futures crate are able to elegantly model the large majority of places where concurrency is needed, and they are nicely compostable. More than once, we have wanted to improve the performance of some path and been able to lift the sequential model into a stream, evaluated concurrently with some max buffer size. From there, converting to true parallel execution is just a matter of wrapping the looped futures in Tasks. Obviously just sprinkling an async on it isn’t going to make anything faster (it just converts your function into a state-machine generator that then needs to be driven to completion). But being able to easily and progressively move code from sequential to concurrent to parallel execution makes for significant performance gains.
- time4tea 6mo agoNo mention of JVM.. which is a bit odd as recently is kinda solved this problem. Sure, not all use cases, but a lot. It uses N:M threading model - where N virtual threads are mapped to M system threads and its all hidden away from you. All the other languages just leak their abstractions to you, java quietly doesn't. Sure, java is kinda ugly language, you can use a different JVM language, all good. Don't get me wrong, love python, rust, dart etc, but JVM is nice for this.
- mirekrusin 6mo agoNo mention of ruby which is colorless.
- x3n0ph3n3 6mo agoIt's baffling why concurrency is so complicated in every other language when Ruby does is so effortlessly.
- ibraheemdev 6mo ago> OS threads are expensive: an operating system thread typically reserves a megabyte of stack space Why is reserving a megabyte of stack space "expensive"? > and takes roughly a millisecond to create I'm not sure where this number is from, it seems off by a few orders of magnitude. On Linux, thread creation is closer to 10 microseconds.
- magicalhippo 6mo ago> Why is reserving a megabyte of stack space "expensive"? Guess it's not a huge issue in these 64-bit days, but back in the 32-bit days it was a real limitation to how many threads you could spin up due to the limited address space. Of course most applications which hit this would override the 1MB default.
- n_e 6mo ago> Why is reserving a megabyte of stack space "expensive"? Because if you use one thread for each of your 10,000 idle sockets you will use 10GB to do nothing. So you'll want to use a better architecture such as a thread pool. And if you want your better architecture to be generic and ergonomic, you'll end up with async or green threads.
- wmf 6mo agoOn a 64-bit system, 10 GB of address space is nothing.
- matheusmoreira 6mo ago10 GB of RAM is certainly something though. Especially in current times.
- monocasa 6mo agoExcept if those threads are actually faulting in all of that memory and making it resident, they'd be doing the same thing, just on the heap, for a classic async coroutine style application.
- holybbbb 6mo agoNo mention of Novell Netware. This was a solved problem decades ago and Windows had it for almost as long. The next decade will be a proliferation of hackers having fun with io_uring coming up with all sorts of patterns.
- lelanthran 6mo ago> No mention of Novell Netware. This was a solved problem decades ago and Windows had it for almost as long. Solved at the OS layer, sure. This article is about solving it at the language layer. It's been solved for a long time at the OS layer. I mean, didn't VAX have something like overlapped IO in 1980? Unix had asynchronous IO as well with `select`.
- teaearlgraycold 6mo agoNot a fan of async in other languages (I avoid it in rust and python like the plague), but it feels like a straight upgrade in JS. I’ve never once regretted its addition. In my experience it’s extremely rare for things to get more complicated than an await followed by a Promise.all(). Unhandled rejections are super obvious to a human as performing a .then() chain is uncommon in the days of await. And linters will pick it up if you miss it. Function coloring isn’t an issue as all of the Node stdlib that I’ve seen provides async functionality (back in the day you could accidentally call a synchronous file system operation and break the event loop). You end up with everything returning a promise except for some business logic at the leafs of the dependency graph. A Node app is mostly i/o anyway, thus the functions mostly return Promises. The await keyword is homomorphic across promises and other values. And type checking (who isn’t using typescript?) will catch most API changes where something becomes async. I can’t say it’s perfect, but it’s really not a problem for me.
- kibwen 6mo ago> Language designers who studied the async/await experience in other ecosystems concluded that the costs of function coloring outweigh the benefits and chose different paths. Not really. The author provides Go as evidence, but Go's CSP-based approach far predates the popularity of async/await. Meanwhile, Zig's approach still has function coloring, it's just that one color is "I/O function" and the other is "non-I/O function". And this isn't a problem! Function coloring is fine in many contexts, especially in languages that seek to give the user low-level control! I feel like I'm taking crazy pills every time people harp about function coloring as though it were something deplorable. It's just a bad way of talking about effect systems, which are extremely useful. And sure, if you want to have a high-level managed language like Go with an intrusive runtime, then you can build an abstraction that dynamically papers over the difference at some runtime cost (this is probably the uniformly correct choice for high-level languages, like dynamic or scripting languages (although it must be said that Go's approach to concurrency in general leaves much to be desired (I'm begging people to learn about structured concurrency))).
- tekacs 6mo agoI mean Java's Loom feels like the 'ultimate' example of the latter for the _ordinary_ programmer, in that it effectively leaves you just doing what looks like completely normal threads however you so please, and it all 'just works'.
- ysleepy 6mo agoJava has gone full circle. Java had green threads in 1997, removed them in 2000 and brought them back properly now as virtual threads. I'm kinda glad they've sat out the async mania, with virtual threads/goroutines, the async stuff just feels like lipstick on a pig. Debugging, stacktrackes etc. are just jumbled.
- gf000 6mo agoI don't think comparing 97's green threads to virtual threads ever made sense. Like their purpose/implementation everything is just so different, they don't share anything at all.
- jdw64 6mo ago[dead]
- threethirtytwo 6mo agoThis was a hardware and os level problem first. All of that had to be solved before higher level abstractions through languages like go JavaScript could tackle it. Author skipped this entirely.
- oconnor663 6mo ago> async/await introduced entirely new categories of bugs that threads don’t have. O’Connor documents a class of async Rust deadlocks he calls “futurelocks” I didn't coin that term, the Oxide folks did: https://rfd.shared.oxide.computer/rfd/0609 https://rfd.shared.oxide.computer/rfd/0609. I want to emphasize that I don't think futurelocks represent a "fundamental mistake" or anything like that in Rust's async model. Instead, I believe they can be fixed reliably with a combination of some new lint rules and some replacement helper functions and macros that play nicely with the lints. The one part of async Rust that I think will need somewhat painful changes is Stream/AsyncIterator (https://github.com/rust-lang/rust/issues/79024#issuecomment-4264112024 https://github.com/rust-lang/rust/issues/79024#issuecomment-...), but those aren't yet stable, so hopefully some transition pain is tolerable there. > The pattern scales poorly beyond small examples. In a real application with dozens of async calls, determining which operations are independent and can be parallelized requires the programmer to manually analyze dependencies and restructure the code accordingly. I think Rust is in an interesting position here. On the one hand, running things concurrently absolutely does take deliberate effort on the programmer's part. (As it does with threads or goroutines.) But on the other hand, we have the borrow checker and its strict aliasing rules watching our back when we do choose to put in that effort. Writing any sort of Rust program comes with cognitive overhead to keep the aliasing and mutation details straight. But since we pay that overhead either way (for better or worse), the additional complexity of making things parallel or concurrent is actually a lot less. > At the function level, adding a single i/o call to a previously synchronous function changes its signature, its return type, and its calling convention. Every caller must be updated, and their callers must be updated. This is part of the original function coloring story in JS ("you can only call a red function from within another red function") that I think gets over-applied to other languages. You absolutely can call an async function from a regular function in Rust, by spinning up a runtime and using `block_on` or similar. You can also call a regular function from an async function by using `spawn_blocking` or similar. It's not wonderful style to cross back and forth across that boundary all the time, and it's not free either. (Tokio can also get mad at you if you nest runtimes within one another on the same thread.) But in general you don't need to refactor your whole codebase the first time you run into a mismatch here.
- shortercode 6mo agoHaving lived through the changes from callback hell, early promises and then async/await I only ever found each step an improvement and the negatives are very minor when actually working with them. Now function colouring is interesting but not for the reason these articles get excited. Recolouring is easy and has basically no impact on code maintenance. BUT if you need that code path to really fly then marking it as async is a killer, as all those tiny little promises add tiny delays in the form of many tasks. Which add up to performance problems on hot code paths. This is particularly frustrating if functions are sometimes async, like lazy loaders or similar cache things. To get around this you can either use callbacks instead or use selective promise chaining to only use promises when you get a promise. Both strategies can be messy and trip up people who don’t understand these careful design decisions. One other fun thing is indexeddb plays terribly with promises, as it uses a “transactions close at end of task” mechanism, making certain common patterns impossible with promises due to how they behave with the task system. Although some API designers have come up with ways around this to give you promise interfaces for databases. Normally by using callbacks internally and only doing one operation per transaction.
- quietbritishjim 6mo ago> all those tiny little promises add tiny delays in the form of many tasks. That depends on the language/framework. In some languages, `await foo()` is equivalent to `Future f = foo(); await f`. In others (e.g. Python), it's a primitive operation and you have to use a different syntax if you want to create a future/task. In Trio (an excellent Python alternative to asyncio), there isn't even the concept of a future at all!
- ambicapter 6mo ago> all those tiny little promises add tiny delays in the form of many tasks Is this because the functions are async or is that because most of the time async is used for things that are I/O like and therefore susceptible to these kinds of delays?
- Rohansi 6mo ago> This is particularly frustrating if functions are sometimes async, like lazy loaders or similar cache things. This is a solved problem in C#. You can use ValueTask<T> instead of Task<T> and no promise will be allocated if it never awaits.
- twoodfin 6mo agoHow did this article get back on the front page with all its comments time-shifted? My trite slop bashing was days ago: https://news.ycombinator.com/item?id=47862726 https://news.ycombinator.com/item?id=47862726
- aw1621107 6mo ago> How did this article get back on the front page with all its comments time-shifted? Sounds like the second-chance pool (dang's description at https://news.ycombinator.com/item?id=26998308 https://news.ycombinator.com/item?id=26998308)
- coolThingsFirst 6mo agoPromised delivered exactly what it it.
- worik 6mo agoThere is a small "straw man" bias here. Callbacks are not the only alternative to Promises. There exist state machines and event loops too. I play around with real time audio, and use state machine/event loop. A very powerful, if verbose, method to do real-time programming, I cannot see how asyc/await could achieve the same ends
- worik 6mo agoI would point out two other short Cummings in the async/await paradigm 1. It makes asynchronous programming look synchronous. I do not like things being other than they appear. The point was touched on with the: getOrders(user.id), getRecommendations(user.id) example, but it is a serious thing when the v mental model is wrong 2. On a related issue CPU bound code can block the thread of execution and stop any concurrency in its tracks In Rust there is the added problem of shoehorning it into the memory model which has lead to a lot of hairy code and tortured paradigms (e.g. pin)
- jayd16 6mo agoThey get their sequential trap example wrong. You can call async methods without immediately calling await. You can naively await as late as possible. They'll run in parallel, or at least how ever the call was configured.
- cbarrick 6mo agoWell, it depends on the language. In Javascript, promises are eager and start executing immediately. They return control back to the caller when they need to wait. So in practice, all of your promises are running concurrently as soon as you create them. In Rust, futures are lazy don't start executing until they are awaited. You have to use various features of your chosen runtime to run multiple futures concurrently (functions like `spawn` or `select`). But that interface isn't standardized and leads to the the ecosystem fragmentation issue discussed in the article. There was an attempt to standardize the interface in the `futures` crate, but none of the major runtimes actually implement the interface.
- mkj 6mo ago> Tokio’s dominance is function coloring at ecosystem scale That isn't function colouring, but rather plain incompatible APIs/runtime. You could have the equivalent with non-async ecosystems.
- twoodfin 6mo agoWhat it really is: LLM-generated puffery.
- jen20 6mo agoIt seems unfair to spend so much time in this article talking about JavaScript and Java without mentioning that async/await first appeared in .NET, and _broadly speaking_ works pretty well there.
- Waterluvian 6mo agoI’m not really smart on this subject but I started during callback hell and now use async in Node and front-end and I find it to be just superb. Sometimes I have to reason about queued tasks vs. micro tasks and all that but most of the time it just does what I expect and keeps the code very clean.
- pyinstallwoes 6mo agoErlang is a beautiful example of not having to deal with function coloring/creep. Any other language?
- jemfinch 6mo ago> OS threads are expensive: an operating system thread typically reserves a megabyte of stack space and takes roughly a millisecond to create. It's typically less than a hundred kilobytes and (on the systems I've benchmarked using std::thread) it takes 60usec (wall time in userspace) to create and destroy a thread. Threads have gotten so fast that paying the async function coloring price makes very little sense for most software.
- smallstepforman 6mo agoIts the stack space allocated to each thread that prevents you from spawning more than a thousand threads. Strategies like a thread per network connection do not scale.
- jemfinch 6mo agoI regularly spawn several thousands of threads in the C++ servers I write, and they perform well. At least 40% of FAANG companies just reduce the size of their per-thread stacks. "Thread per-connection" works just fine, and when you need to go faster, thread pools work even better without coloring all your functions. There is an extremely small set of domains where simple threading doesn't suffice, and async/await is too high a price to pay across the entire software ecosystem just to slightly optimize those domains.
- jcalvinowens 6mo ago> Threads have gotten so fast that paying the async function coloring price makes very little sense for most software. I agree with this 100%. If you're getting paid to bin pack jobs that do lots of concurrent I/O into a server infrastructure, then yes, building these complex async "machines" and forcing everyone to do the extra labor to use them is necessary to avoid a lot of waste. But for everyone else, it's a huge waste of time. Even low end modern embedded Linux systems are capable of running thousands of threads. Synchronous code is always simpler to write, easier to reason about, more straightforward to test, and faster to debug. Nobody should ever reach for async by default.
- miiiiiike 6mo agoJavaScript developers don't like hearing this but RxJS solves, or gives you the tools to solve, most of these problems.
- FpUser 6mo agoin real life when request handler call async/colored/whatnot it lets the call proceed and immediately ready to process next request. The backend then would have no problems to create ever growing number of asyncs currently in flight. In real life those asyncs would most likely end up calling database. The end result is that backend would simply overwhelm the database and other resources that have to maintain states of those countless asyncs in flight. This whole thing is basically snake oil. The best thing backend can do instead is have dedicated thread pool where each real thread has its own queue of limited size. Each element in queue would contain input and output state of request and code to deal with those. Once queue grows over certain size the backend should simply immediately return error code (too busy). Much more sound strategy in my opinion. There are more complex cases of course (like computationally expensive requests with no io that take long time). Handling those would require some extra logic. Async stuff however will not help here either
- shmerl 6mo agoSo what is the next step in solving it that's better previous ones?
- kmeisthax 6mo ago> This is a promise (JavaScript) or future (Java, Rust, etc). The concept dates to Baker and Hewitt in 1977, but it took the C10K pressure of the 2010s to push it into mainstream programming. Almost. JavaScript adopted async because it was a programming language designed to slot into someone else's event loop. Other programming languages, at least on the server, that needed lightweight threading didn't bother with any of this, they just shipped their own managed stacks. But UI code practically demands to own its own event loop and requires everything else live as callbacks inside of it. And JavaScript, because it was designed to live in a browser, inherited these same semantics.
- foreman_ 6mo ago[flagged]
- spacechild1 6mo agoUnfortunately, asynchronous programming is almost always discussed in terms of network I/O. However, there are many more use cases for concurrency. Coroutines can be extremely useful for modelling state machines or any kind of process that happens over time. Everytime you need to do X, then wait for N (milli)seconds, then do Y, etc., coroutines provide a very ergonomic solution. If your language supports stackful coroutines (e.g. Lua or Ruby), you don't even need to color your functions: you can just write regular functions that yield back to the scheduler anywhere in the call stack. To give a concrete example: computer music languages, such as SuperCollider, need concurrency to implement musical scheduling. Imagine a musical sequence where you play a note, wait N beats, play another note, etc. Often you want to play many such sequences simultaneously. Stackful coroutines provide a very elegant solution to this problem. Every independent musical sequence can be modelled by a coroutine that yields everytime it needs to wait. The yielded value is interpreted by the scheduler as a delta time after which the function should be resumed. In this sense, SuperCollider users have been doing async programming since the early 2000s, long before it became mainstream.
- dzonga 6mo agothe best async story is probably Ruby with Fibers. you skip the function coloring. Virtual threads in JVM are similar.
- xgulfie 6mo agoIf I'm writing JavaScript, the cognitive overhead of doing async is less than dealing with nodejs require() and import syntaxes and module compatibility
- Panzerschrek 6mo agoSurprisingly no approach with stackful coroutines was mentioned. It allows avoiding function coloring problem.
- cryptos 6mo agoI think that Java has a really good solution to offer: Virtual Threads. They share the same interface with native threads and free developers from the burdens of async programming. You only need to take care of some things like accidental thread pinning (through legacy code) and the use of ThreadLocal, but otherwise this complexity is hidden. https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-DC4306FC-D6C1-4BCC-AECE-48C32C1A8DAA https://docs.oracle.com/en/java/javase/21/core/virtual-threa...
- edmondx 6mo agoOf all the ways to program with async, I like transparent asynchrony the most. Even though it may seem implicit, it turns out to be the most convenient.