Rust 4 min read

Everyone Loves Rust. Async Rust Is Still a Half-Built Bridge.

Rust has reached the kind of cultural status where criticizing it gets you side-eyed on Hacker News. Memory safety, zero-cost abstractions, a Stack Overflow “most loved language” streak that refuses to end. And yet, beneath the praise chorus, the same quiet grumble keeps surfacing: async Rust is still stuck in MVP territory.

Anyone who has actually shipped async Rust in production knows the feeling. Today, let’s talk about it honestly.

The joke that isn’t really a joke

There’s a running gag in the Rust community: sync Rust is heaven, async Rust is hell. It lands because it’s true.

In sync Rust, the compiler is a strict but fair friend. The borrow checker is picky, but it’s consistent, and the error messages have genuinely improved year over year. Slap an async keyword in front of a function, though, and suddenly you’re knee-deep in Pin, Send, Sync, and mysterious 'static lifetime requirements. The infamous “future cannot be sent between threads safely” error has eaten more senior-engineer afternoons than anyone wants to admit.

This is not a beginner problem. Engineers with five-plus years of Rust still freeze in front of async type errors. That’s a tell.

No standard runtime, no exit ramp

The most cited issue is the absence of a standard runtime. Go ships goroutines in the language. JavaScript has the V8 event loop baked in. Rust took a different path: ship async/await syntax in the standard library, then punt the actual executor to third-party crates.

The result is a fragmented runtime landscape — Tokio, async-std, smol, Glommio — where Tokio has effectively won, but as a de facto monopoly rather than an official standard. Library authors still have to decide which runtime to assume. Cross-runtime compatibility is a recurring headache. A weird two-tier ecosystem has emerged where many crates silently require Tokio, and switching runtimes can mean rewriting your dependency tree.

Async traits: a seven-year homework assignment

For years, the canonical complaint was simple: you couldn’t write async functions inside traits. The workaround was the async-trait macro crate, which became so universal that it might as well have been part of the standard library. The catch: every call boxed the future on the heap. In a language whose entire pitch is zero-cost abstraction, async users had to swallow allocation overhead just to compose code normally.

async fn in trait finally stabilized recently, which is genuinely good news. But the moment you need dynamic dispatch — a trait object holding async methods — you’re back to workarounds. Hence the pattern: the basic case works, the real-world case still requires gymnastics.

Cancellation and Pin: the two landmines

Two more concepts make async Rust genuinely hard, not just verbose.

First, cancel safety. Inside something like tokio::select!, a future can be dropped at any await point. If your code leaves intermediate state behind — a half-written buffer, an unreleased lock, a database transaction in limbo — you get a class of bug that simply does not exist in synchronous code. The type system does not catch it. The compiler does not warn you. You learn through outages.

Second, Pin. It exists to make self-referential futures sound, and the official line is that ordinary users should never have to touch it directly. In practice, the moment you write a non-trivial library, you collide with Pin<&mut Self>, projections, and pinning APIs that read like a category theory final exam. The docs are accurate. They are also impenetrable on first read. And on second read.

So why does everyone still ship it?

Fair question. The honest answer: the alternatives are worse. C++ coroutines are more cryptic. Go is ergonomic but garbage-collected, which rules it out for a lot of systems work. JavaScript isn’t a systems language at all. If you need a high-performance network service with memory safety guarantees, async Rust — warts and all — is on the short list of one.

The trouble is that “no real alternative” has become a way to silence the critique. Point out a rough edge and the reflex reply is “what would you use instead?” Meanwhile, design debt that should have been paid down years ago keeps compounding.

The takeaway

Rust is a great language. It will not become a greater language by pretending its async story is finished. The runtime situation is unstandardized. Trait compatibility is half-solved. Cancellation and Pin still bite veterans.

There are two ways to love a language: defend its flaws, or acknowledge them and help sand them down. If you’re writing Rust today, or weighing whether to adopt it, sit with this question for a minute. The code we currently call “idiomatic Rust” — will it still look idiomatic in five years?

Rust async programming languages developer tools systems programming

Comments

    Loading comments...