tokio::select! Finally Gets rustfmt Support: A Hands-On Look at better_tokio_select

Table of Contents
If you write async Rust, you’ve probably been through this arc: you discover tokio::select!, think it’s brilliant, use it everywhere, then a few months later you realize you dread opening files that contain it.
Not because the logic is hard. Because the code looks like a mess.
1. An Unformatted Macro Is Like an Unwashed Dish
You write a tokio::select! block, hit save, and wait for your editor to clean up the indentation and line widths. Nothing happens.
This isn’t your editor’s fault. It’s rustfmt’s problem.
tokio::select! uses a custom DSL internally. rustfmt doesn’t recognize it at all. To the formatter, this macro is just a blob of unparseable characters. It skips over it and pretends it doesn’t exist.
It’s like having a pile of unwashed dishes in your kitchen. The dishes work fine, they hold food perfectly. But every time you walk past them, you feel a little annoyed. You just don’t want to deal with it.
2. better_tokio_select Enters the Scene
In late March 2026, a crate called better_tokio_select showed up on the Rust community forum. The author is nik-rev, and the goal is laser-focused: make tokio::select! formattable by rustfmt.
It exports a macro called tokio_select! that does everything tokio::select! does. Conditional branches, biased selection, else fallbacks — all there.
The difference is in the syntax.
Here’s tokio::select!:
tokio::select! {
Ok(res) = reader.read(&mut buf), if can_read => {
writer.write_all(res.bytes).await?;
}
_ = shutdown.recv() => {
return Ok(());
}
}
And here’s tokio_select!:
tokio_select!(match .. {
.. if let Ok(res) = reader.read(&mut buf) && can_read => {
writer.write_all(res.bytes).await?;
}
.. if let _ = shutdown.recv() => {
return Ok(());
}
})
See those .. patterns? They look a bit odd. But they turn the whole expression into valid Rust syntax, which means rustfmt can finally parse it and format it.
3. Why Can’t rustfmt Handle tokio::select!
The reason is pretty straightforward.
rustfmt works by parsing the Rust syntax tree and reformatting it according to rules. It handles function bodies, match statements, if expressions — all the “standard” syntax. But macros are different.
When rustfmt encounters a macro call, it checks whether the macro’s arguments are valid Rust expressions. If they are, it formats them. If not, it skips.
tokio::select!’s arguments use a custom syntax: <pattern> = <expr>, if <cond> => <handler>. This syntax has no corresponding expression form in Rust, so rustfmt can’t do anything with it.
better_tokio_select’s trick is clever: it writes branches as arms of a match expression, and match is standard syntax that rustfmt fully supports. The .. patterns are valid Rust too — they’re the rest pattern.
Think of it this way: you want the delivery driver to leave your package at the door, but your address is written in Klingon. The driver can’t read it, so the package goes back. Now you’ve switched to a normal address, and the package arrives.
4. What Does It Look Like in Practice
Let’s compare a few real-world scenarios.
TCP Proxy: Read/Write + Graceful Shutdown
This is one of the most common patterns in network programming: reading data on one side, listening for a shutdown signal on the other.
With tokio::select!:
tokio::select! {
res = reader.read(&mut buf), if can_read => {
let n = res?;
if n == 0 { return Ok(()); }
writer.write_all(&buf[..n]).await?;
}
_ = shutdown.recv() => {
return Ok(());
}
}
With tokio_select!:
tokio_select!(match .. {
.. if let Ok(n) = reader.read(&mut buf) && can_read => {
let n = res?;
if n == 0 { return Ok(()); }
writer.write_all(&buf[..n]).await?;
}
.. if let _ = shutdown.recv() => {
return Ok(());
}
})
Same logic, slightly different syntax. But the second one can be formatted by rustfmt with a single command.
Message Handler: Prioritized Selection
In message queue scenarios, you might want to process certain message types first. The biased keyword makes select check branches top-to-bottom in order, instead of polling randomly.
tokio_select!(biased, match .. {
.. if let Some(Message::Data { id, payload }) = rx.recv() => {
process(id, payload).await;
}
.. if let Some(Message::Control(cmd)) = rx.recv() => {
handle_command(cmd).await;
}
_ => {
println!("no messages pending");
tokio::time::sleep(Duration::from_millis(50)).await;
}
})
biased goes in as the first argument, followed by the match .. expression. The structure is clean and readable, and the formatter handles it properly.
Timeout Control: The Most Common Async Pattern
Set a timeout, give up if it expires. This pattern is everywhere in async Rust.
tokio_select!(match .. {
.. if let Ok(result) = fetch_data(url) => {
println!("got data: {:?}", result);
}
.. if let _ = tokio::time::sleep(Duration::from_secs(5)) => {
println!("request timed out");
}
})
5. Adding It to Your Project
Usage is simple. Add one line to Cargo.toml:
[dependencies]
better_tokio_select = "0.2"
Then import in your code:
use better_tokio_select::tokio_select;
If you don’t want to write use every time, you can do a global import:
#[macro_use(tokio_select)]
extern crate better_tokio_select;
Minimum supported Rust version is 1.71, dual-licensed under Apache-2.0 and MIT.
6. better_tokio_select Isn’t the Only Option
better_tokio_select isn’t the only crate tackling this problem. The Rust ecosystem has a few similar efforts.
selectme (v0.7.1): Focuses on performance and fairness. Its select! macro syntax is nearly identical to tokio::select!, but the internal implementation is optimized. It also supports an inline! macro for more fine-grained control flow management. Worth a look if performance is your top priority.
tokio-alt-select: Written by Dioxus author jkelleyrtp, it replaces the custom DSL with closure syntax. The style feels more like a regular function call, and rustfmt handles it fine. The syntax difference is significant though, so your team would need to adapt.
All three crates solve the same problem: the formatting dead zone of tokio::select!. The differences are in syntax style and extra features. Pick whichever suits your team’s taste.
7. When Should You Switch to better_tokio_select
Honestly, if your select! blocks are only two or three lines, the original is fine. Don’t bother.
But if you’re seeing these signals, it’s worth considering:
select!blocks over 20 lines with more than 4 branches- Multiple people on the team, and formatting inconsistency is a headache
- You run
cargo fmtbut theselect!blocks are always a mess - Code reviews spend more time on formatting than on logic
These are small things individually. But small things accumulate into friction. Good tools remove that friction.
FAQ
Is there a performance difference between better_tokio_select and tokio::select!?
No. better_tokio_select is pure syntax sugar. The compiled output is identical to what tokio::select! generates. No runtime overhead.
Why not use selectme instead?
selectme is a good project, but its syntax is nearly identical to tokio::select!, which means it has the same rustfmt compatibility problem. If formatting is your main concern, better_tokio_select is more direct.
Do the .. patterns hurt readability?
They look strange at first, but you get used to them quickly. The overall readability improvement from proper formatting far outweighs the initial weirdness of ... Having one consistent style across the team beats everyone writing their own indentation.
