Basically, async Rust requires you to understand how “async” works under the hood. Javascript async does stuff automatically, there is an implicit runtime and you can just await or .then and get a Promise and closure which encapsulates whatever data you need and stores it on the heap.
Rust lets you optimize the runtime and do polling etc. how you want. But you have to do everything explicitly. And you can store futures on the stack and customize how their represented (including the closures), but you have problems like async traits because every future has a different type.
Lots of people say “async Rust is hard because async is hard”. Honestly this is false, async is easy if you Box dyn everything and use the tokio runtime. Async Rust is hard - and Rust in general is hard - because they don’t compromise performance for abstraction and encapsulation. You get exposed to the gritty internals of how async works in exchange for being able to make futures and a runtime which are optimal for your specific program.
> Async Rust is hard - and Rust in general is hard - because they don’t compromise performance for abstraction and encapsulation.
I love Rust, but this is not completely true. Async Rust is hard because many nice features from Rust are not available in async Rust. For example, you can't have async functions in traits and always need to box return value, compromising performance just because the language is not powerful enough yet to support this.
This also leads to not having standard Read and Write traits and a bunch of fragmentation between runtimes. It's not that having it would compromise performance, it would even allow better performance than the current workarounds, but async Rust is still in development and needs time to catch up. In the meantime you need to do a bunch of awkward tradeoffs between ergonomics and performance when writing async Rust and of course it's frustrating to developers that are used to Rust being zero-cost with awesome ergonomics.
This is very similar to c++. After transitioning to coroutine from previously semicomplete implementations like promises, our codebase become so much better. It just takes time to reach a better feature
JavaScript also has the benefit of a single threaded event loop - that eliminates a whole class of complexity and dealing with schedulers etc. - there is only one.
Async already gets messier in C# for example which also has a GC.
This is a nice read on why you likely should prefer a single threaded runtime and enable parallelism through other means. Or decide to pay the synchronization cost.
if you write harder concurrent program in JS, single threaded event loop does not help. It still has also same concurrency problems.
e.g. if there is shared global mutable state:
a) in workflow 1 you have, do_some_work_on_global_state; do some IO; in IO callback, finish more do_some_work_on_global_state.
b) in workflow 2 .. same like above work on global_state.
Now, in IO callback, you don't know if workflow 2 ran and have to handle all possible combinations of global_state above.
Replace global_state with common_state and problem still remains.
If you don't have common_state between multiple workflows, then it is not a hard concurrency problem and should be easy to do in all languages.
Sure it does, you don't have to manage on which thread you handle continuations (which you must in multithreaded GUI for example) - there's only one scheduler which simplifies the async API a lot.
But even for concurrency - single threaded event loop/cooperative multitasking eliminates a whole class of partial state updates and synchronization primitive/locking errors - it's not even close to preemptive multitasking complexity.
Rust does not require you to know how async works under the hood.
Javascript async doing things automatically has been infinitely more confusing for me, frankly. Async rust isn't hard, rust isn't hard - not for a lot of people at least.
I don't know what gritty details you're referring to, you need to know the same rules you always know - move semantics, some concept of lifetimes maybe. Move of the time it's "add a `move` and clone before the async block".
In general I agree that the difficulty and need to know the "gritty details" is overstated, but there is one aspect where that is true and I have found it confusing at times. Since it has to capture anything in scope of an await point, you will sometimes get somewhat non-obvious compiler errors about how "X is not Send" when it's not really obvious at all why it would need to be. So something like
```
let locked = std::sync::RwLock<Foo> = ...;
let lock = locked.write().unwrap();
bar.doSomethingAsync().await
```
will complain because `std::sync::RwLockWriteGuard` is not send. Just looking at the code it is not really clear why it should need to be. To understand why, you need to understand how the compiler transforms this code into a state machine and capture everything in scope of an await point in Struct that must be send (since it can shift to new thread when resuming). It makes sense when you understand what's happening under the hood but can be a bit baffling when you are starting out.
That is tricky. It's also something you need to know when working with closures in Rust, which are for the same reason much harder to work with and understand than closures in other languages. I wonder whether it would have been better design for Rust closures to require an explicit capture list, like in C++, just to be more explicit about what is happening. (Not sure if/how that would translate to `await`.)
Fine-grained logic in async JavaScript can be a very special kind of pain. It's a rather specialized event loop, but the vast majority of articles treat it like "oh it's just a normal in-order event loop like every other".
It ain't. Unless your logic has no order requirements between async components, or explicitly accounts for things like "microtasks", there's a chance it's wrong... and it depends on your runtime: https://bytefish.medium.com/the-execution-order-of-asynchron...
(it's generally better to not depend on execution order in async systems anyway, but it's rather easy for it to sneak in sometimes. if it does, it may work on your machine but not on mine, or it might change based on what kinds of tasks other code spawns, if you press a button at a critical moment, etc)
I made a fairly complex app using tokio and async. I did not know how async worked in rust at the time. I didn't even know entirely why I needed tokio.
Sure, but in the common case, "everything" is just adding a simple attribute to your main() function (e.g. `#[tokyo::main]`), and adding the `async` keyword to functions that have stuff in them that need `.await`ing. That's... kinda it?
The only real difficulty I've run into is when I have multiple futures I need to wait on, since there are some fiddly bits to deal with (like using select!{} can cause you to lose data depending on what the underlying futures are doing).
Regardless, comparing Rust to Javascript is a bit weird; they are just not comparable languages with even remotely similar intended use cases.
Rust lets you optimize the runtime and do polling etc. how you want. But you have to do everything explicitly. And you can store futures on the stack and customize how their represented (including the closures), but you have problems like async traits because every future has a different type.
Lots of people say “async Rust is hard because async is hard”. Honestly this is false, async is easy if you Box dyn everything and use the tokio runtime. Async Rust is hard - and Rust in general is hard - because they don’t compromise performance for abstraction and encapsulation. You get exposed to the gritty internals of how async works in exchange for being able to make futures and a runtime which are optimal for your specific program.