Aysnc/await in Rust is a couple years old now. Personally, I was very much into async Rust at first. Over the years, I've come to slowly dislike it. Now, I actively avoid it. In this article I will try and lay out the reasons for that. I have written plenty of async Rust code (about 100k lines async Rust, 50k non-async). I was partially inspired by many others who have similar thoughts about async/await.
Let's start with a quick refresher of the "story" of async/await and how it came to be (or at least how I understand it): Around the turn of the century, as the web started to grow and more and more people came online, there was a need for faster web servers. More specifically, developers wanted to optimize the maximum number of concurrent clients a web server could serve. This problem was dubbed the C10k problem. "How to serve more than 10 thousand requests simultaneously?". The most basic web server works like this: It opens a server socket, accepts incoming connections, starts a thread for each of them. The thread then handles the request, and stops when it is done. If it wants to do some I/O, like sending or receiving, it will just wait for that operation to complete, it will block. The OS will handle the scheduling between these threads. This approach is not optimal. Spawning a thread is somewhat slow, and handling 10k simultaneous connections means doing it a lot. To get better performance, we need to reach for non-blocking I/O. On Linux, non-blocking I/O is provided by poll and epoll. The latter of which is now the de facto standard (io_uring is more modern and gaining traction). On Windows there's IOCP, and on macOS and the BSDs, there's kqueue. All of these are roughly similar: They are just a list of sockets. The programmer adds a socket and what operation it wants to do. Then the program polls the list as a whole. Any time any of the sockets are "ready", they are notified. This way, many sockets can be managed from just one single thread. This is much faster since there is no need to spawn OS threads.
This paradigm has an issue though: It is much more complicated to program. Before, you had this nice per-request linear function flow. But with non-blocking I/O, the programmer has to manage the state of all these sockets from a single point. A possible solution to this is the callback-based approach. For each event, you pass in a closure that handles that event. The main thread just calls that callback whenever it has polled an event. This is the approach NodeJS took, and still does to this day. Unfortunately, it has some usability problems too, colloquially known as "callback hell". It even has its own website.