Most of the pain here come from the unholy trifactor: combining async, lifetimes and dynamic dispatch with trait object closures; which is indeed very awkward in practice.
Async support is incredibly half-baked. It was released as an MVP, but that MVP has not improved notably in almost three years.
There are lots of ideas, some progress on the fundamental language features required (GAT, existential types, ...) and the random RFC here and there, but progress is painfully slow.
This isn't because no one cares, but because Rusts async implementation (which is very cool due to the low overhead) and its interactions with the other language features require complicated extensions to the type system. It does seem to me like there might be a lack of resources/coordination/vision ever since the Mozilla layoffs, but that's a different topic.
If you can avoid async I would recommend doing so. The problem is that the entire ecosystem has completely shifted to async. There are almost no active / popular libraries related to network IO that haven't switched over.
To be clear: using async is fine if you know what you are doing, and it can provide incredible performance. But if you do, keep it simple: avoid lifetimes and most importantly: don't attempt advanced trait shenanigans - if you do need traits, just returned BoxFutures without lifetimes, throw in lots of clone(), share as little data as possible, use Arc<tokio::sync::Mutex<_>>, and call it a day.