Hacker News new | ask | show | jobs
by traderj0e 40 days ago
Is async in Rust really this bad? Last time I used Rust was before that existed. I know it's a pain in Python because they bolted it on way after, but in JS it's a breeze because everything standardized on promises early.
5 comments

> Is async in Rust really this bad?

No, it's not. It works. Perfect? No, absolutely not. There is plenty you could improve, plenty of rough edges you could smooth out. Stuff that caused us problems at the job I had writing low-ish level machine control services. But it's totally workable and we were able to ship working devices, especially compared to doing async stuff in other most other languages, especially the memory-unmanaged ones.

Kind of like Rust itself, a ton of people have tried it and bounced off it because they couldn't get it working in 10 minutes, and in doing so have declared it impossible/for geniuses only/broken/ecosystem-destroying. The narrative around async Rust is probably 70% meme/bad PR, 30% real, actual issues that could be improved.

I hope this comes off as fair. I don't want to excuse any of the shortcomings, but it's a working, useful tool.

I think part of it is that most of Rust sets a really high standard for polish and good design; developers new to the language are often quite impressed by its expressiveness and by the breadth of bug classes it protects you from, once you get over the initial learning curve. (Not always—some developers never grow to like it, either because it's a bad fit for their use case or because they happen to find its particular forms of ceremony especially off-putting—but the success cases are striking.)

And then you start using async, which is less polished and has more awkward design compromises and more footguns that you only find out about at runtime, and it's a bit of a disappointment by comparison, even if some of the problems aren't worse than what you find in competing languages. This is the vibe you get in the Oxide RFDs about things like futurelock, for example.

100% agree with your take, speaking as a professional async rust writer for like five years now.

I’ll add to it that structured concurrency patterns in async Rust can be legitimately awesome and very fun, once you’ve bounced off the traits in the futures crate enough times to use it in anger. The type system can be annoying, but as usual, it’s almost always technically right, and once you understand the ownership flow required for your nifty chain of future combinators, is not too too bad.

the best async/concurrency model i know is verse [1], a language made for fortnite scripting because tim sweeney decided he wants something with "metaverse" in the name. instead of async/await or verbose callbacks its all combinators. from their docs:

  # Nested blocks for complex operations
  sync:
    block:  # Task 1 - sequential operations
        LoadTexture()
        ApplyTexture()
    block:  # Task 2 - parallel to task 1
        LoadSound()
        PlaySound()
    LoadModel()  # Task 3 - parallel to tasks 1 and 2
[1] https://verselang.github.io/book/14_concurrency/#structured-...
Surrender, to compile

Weather the winter storms

You will find, true bliss

Funny... I started off with very little knowledge, totally cheated with what I wanted to do by cloning everything crossing various library boundaries and it still worked surprisingly well. Then I learned about (A)RC, Box etc... and I still kinda really hate the lifetime syntax.

Note: most of what I've used it for has been relatively simple... API's with tokio and axum in rust are emphatically not much more difficult than say C# with FastEndpoints, or JS/TS with Hono. It's a bit different.

Rust’s async makes some design decisions that make it a unique feature: no other language has zero allocations to do async, for example. (In C++’s version you can get it to do no allocations if you do certain things, like making the required allocator a no-op, in my understanding, but it conceptually requires a call to an allocator)

This makes it suitable for a much wider variety of tasks than other languages with similar features, but does mean that there are more details that you need to care about than in other languages that are higher level.

This means it is controversial: some people would prefer a higher level experience, but for those who do use it for its full range of tasks, it’s great.

There are some rough edges, but it’s just a feature that, even outside of Rust, some people just fundamentally dislike. So it draws a lot of heat from all sides.

It is also probably the single largest driver of adoption of the language. Rust started truly taking off once it landed.

I can only speak for myself, but I kind of refused to get started with Rust until async landed... only because it's kind of a core of scalable services. I think it's pretty great in that you can use an async runtime, but still launce native threads if you want/need to for specific scenarios. It's pretty great and for most high level tasks, I wouldn't classify it as significantly more difficult than C#.

A few other things I've done with it have been written by AI as much as myself... which has similarly been pretty nice... Rust code can be very easy to reason with (lifetime syntax not withstanding). For some reason Rust lifetimes burn my soul.

"it’s just a feature that, even outside of Rust, some people just fundamentally dislike" is why I have a hard time gauging this. I know Python users whine about it endlessly just because it's a feature from JS, even though Python's existing concurrency features were the worst of both worlds.

The Rust-specific async sounds interesting. I should give it a try.

If someone finds normal ownership concepts in Rust to be difficult, making the leap to async is only going to make that worse.

I don't think it's friendly to Rust beginners but I also think the complaints about if have been overblown

I think it depends on what you're doing... getting to a point where you understand Arc helps a lot, and if you're mostly using it with something like Axum, there's very little you need to worry about specific to lifetime or a lot of the burrowing logic in practice. You can definitely get by with less than perfect code.
It depends what you compare it to.

If your reference is JS or C#, then Rust's async has more friction and annoying details. But Rust is generally harder due to being low-level and picky about memory management and thread-safety details. In a way it's a compliment that Rust's usability even gets compared to GC languages.

If you compare it to hand-rolling state machines in C, or even callbacks in pre-async Rust, then it's an amazing simplification and a very sweet syntax sugar. Especially when you want async code composable, supporting control flow and cancellation.

If you compare it to naive sync Rust while ignoring the extra capabilities that async adds, then async is more complicated and less feature-complete (Streams have more moving parts than Iterators, generic async closures have trickier syntax than generic sync closures, there are scoped threads but no scoped multi-threaded futures, etc). IMHO the difference isn't large and it's reasonable for what you get.

There's also a view that async/await syntax is fundamentally misguided and shouldn't exist at all. From Rust's perspective that's an agree-to-disagree. Rust tried having green threads first (before 1.0) and decided it was too intrusive and magic for a low-level systems language. Rust tried to make do with just threads, callbacks and macros, but it didn't work well either (user code can't make futures compose and optimize as well without compiler's help, especially when you want borrow checker to work with it).

No it’s not bad at all. If you’re creating a library you might run into some hard problems, but for application code it’s pretty easy.