Rust, part 13: Rust 1.85, edition 2024, and async closures
Part 13from the Rust series · 15 parts in all
February 2025 brought two things at once: Rust 1.85 and the 2024 edition. The edition is the more interesting half, because it is the mechanism that lets the language make breaking changes without breaking anyone — and this time it also carried a feature people had waited a very long time for.
An edition is an opt-in breaking change
# Cargo.toml
[package]
edition = "2024" # was "2021"; per-crate, so a workspace can migrate one crate at a time
Editions change semantics, not syntax availability: crates of different editions link into
one binary, so migration is per-crate and can be done incrementally. cargo fix
--edition performs the mechanical parts, and the honest way to read the result is that
it got your code compiling on the new edition — the behaviour changes still need your
attention.
What 2024 changes is mostly about making previously-accepted-but-wrong code stop compiling:
the lifetime capture rules for impl Trait now capture all in-scope lifetimes
(which fixes real bugs and breaks code that relied on the old narrow rule), unsafe
extern and unsafe attributes must be written unsafe explicitly, the
gen keyword is reserved for generators, and the temporary scope of a tail
expression in a block changed — the one most likely to alter what your code does
rather than what it says.
Async closures, at last
// Before: a closure that returns a future cannot borrow its arguments,
// because it has no way to say "the future borrows from what I captured".
let handler = |xc| async move { save(xc).await };
// 1.85: an async closure, which is a closure whose call returns a future
// borrowing from the closure - the missing piece, and it is FnMut.
let handler = async |xc: Item| -> Result<()> { save(xc).await };
items.for_each(handler); // no `async move` dance, no boxing, no macro
This is the small-looking feature that removes a whole genre of workaround. Retry helpers,
middleware, and anything taking "an async callback" used to require a boxed
Fn() -> BoxFuture from the futures crate, an allocation per
call, and a lifetime bound most people copied without understanding. With
AsyncFn / AsyncFnMut / AsyncFnOnce traits alongside,
it is plain code.
The combination is what makes 1.85 a step change rather than an incremental release: async
closures to pass around asynchrony, plus part 8's async fn in traits, means the
async subset of Rust finally composes without a macro layer holding it together. Next: the
edition's other headline syntax and the run of releases that followed.