Rust Edition 2024: what really changes day to day
Table of contents
- Key takeaways
- How Rust editions work
- Granular capture in closures: the most relevant change
- The expanded 2024 prelude
- More explicit unsafe in extern blocks
- Migration in practice
- My read
- Frequently asked questions
- Can I mix edition 2021 and 2024 crates in the same workspace?
- How long does migrating a real project to edition 2024 take?
- What happens to my extern "C" blocks when moving to Rust 2024?
- Sources
Rust Edition 2024 became stable on February 20, 2025 alongside Rust 1.85. Seven weeks later, the changes that actually matter for daily work are three: more granular variable capture in closures and Return Position Impl Trait, an expanded prelude with Future and AsyncFn, and unsafe now mandatory in extern blocks.
Rust Edition 2024 came out stable on February 20 alongside Rust 1.85, and seven weeks later there’s enough data to talk about what it brings in practice. Rust editions are a curious mechanism: they don’t break compatibility, but they allow changing defaults that would otherwise break existing code. A crate with edition = "2024" compiles against the same compiler as one with edition = "2021", and the two can depend on each other. What changes is how new code is written and some behaviors that adjust language details.
This post is a practical analysis of the changes that most affect daily work, with emphasis on what actually matters for those already using Rust. For the context of Rust’s role in systems beyond userspace, the analysis of Rust in the Linux kernel illustrates the language’s maturity trajectory.
Key takeaways
-
Migration with
cargo fix --editionis the recommended path; it resolves most changes automatically. -
Granular capture in closures (
use<T>in Return Position Impl Trait) eliminates unnecessary lifetime captures. -
The 2024 prelude adds
Future,IntoFuture,AsyncFn*,LazyCell, andLazyLock. -
unsafeinexternblocks is now mandatory;cargo fixmigrates automatically. -
Closures in
async fnno longer require explicitmoveexcept with unusual capture patterns.
How Rust editions work
Editions are Rust’s way of evolving the language without breaking existing code. Rust 1.0 introduced the stability promise: code that compiles today will keep compiling in future versions. Editions are the escape valve for changes that would require breaking that promise if applied globally.
When a crate declares edition = "2024" in Cargo.toml, it opts into that edition’s semantics. Crates with edition = "2021" in the same workspace keep compiling with the previous semantics. The compiler knows each crate’s edition and applies the corresponding rules. There’s never cross-crate breakage: a 2024 crate can depend on a 2021 crate and vice versa.
Migration between editions is assisted by cargo fix --edition, which automatically rewrites code that needs to change. Most real projects migrate from 2021 to 2024 with this command and without significant manual intervention, except for cases with unusual closure capture patterns or complex extern blocks.
Granular capture in closures: the most relevant change
The change with the most impact on real code is the refinement of variable capture in closures and in Return Position Impl Trait (RPIT).
In Rust 2021, when a closure captured something implementing Future, it sometimes captured more than necessary, forcing more restrictive lifetimes than the programmer intended. In 2024, capture is more granular: the compiler captures exactly what’s used, not the whole object if only a field is needed.
The practical consequence is that in 2024, async code that in 2021 required workarounds to satisfy the borrow checker just works. Those workarounds were things like explicitly moving data before the async call, or using Arc where it wasn’t semantically necessary. The number of cases where async move {} was mandatory drops with granular closure capture.
The complement of this is the use<T> syntax in RPIT, which lets you explicitly declare which lifetimes and generic types an opaque return type captures. This eliminates compiler ambiguity about what’s captured, which was a frequent source of hard-to-debug errors in code with intertwined generics and lifetimes.
The expanded 2024 prelude
The prelude is the set of names Rust automatically imports in every module without an explicit use. In 2024, seven async-related types and traits are added:
-
FutureandIntoFuture. -
AsyncFn,AsyncFnMut,AsyncFnOnce. -
LazyCellandLazyLock(thread-safe lazy initialization).
The most immediate impact is that async code no longer needs to explicitly import Future in every file working with it. LazyCell and LazyLock in the prelude reduce the boilerplate of lazy initialization. That pattern was common but verbose with external once_cell. Anyone running real services on axum and tokio will notice the expanded prelude in almost every async handler written from now on.
There’s an important nuance: if a project had identifiers with those same names, the prelude addition can cause name collisions that cargo fix doesn’t always resolve automatically. The scenario is rare but worth checking.
More explicit unsafe in extern blocks
In Rust 2024, extern "C" {} blocks require the explicit unsafe keyword:
// Rust 2021:
extern "C" {
fn puts(s: *const u8) -> i32;
}
// Rust 2024:
unsafe extern "C" {
fn puts(s: *const u8) -> i32;
}
The change reinforces signaling that declarations in extern imply code outside Rust’s borrow checker’s control. The cargo fix --edition command migrates this automatically, so in practice it requires no manual work outside projects with a lot of FFI code.
There’s a useful counterpart: functions inside unsafe extern blocks that the programmer can guarantee are safe to call can be marked safe:
unsafe extern "C" {
safe fn abs(input: i32) -> i32; // can be called without unsafe
unsafe fn gets(s: *mut u8); // still requires unsafe at the call site
}
This improves FFI binding ergonomics without losing the signal of what’s truly unsafe.
Migration in practice
The migration process for a real project:
-
Update the toolchain to 1.85 or higher:
rustup update stable. -
Run
cargo fix --editionin the workspace. -
Change
edition = "2024"in the affectedCargo.tomlfiles. -
Run
cargo testandcargo clippyand address warnings. -
Manually review cases
cargo fixcouldn’t migrate automatically.
In projects with lots of FFI code or unusual closure patterns, step 5 may require a few hours. A typical application or library project without FFI migrates in under thirty minutes.
My read
Rust Edition 2024 is an ergonomics edition, not a headline one. There are no spectacular new features; there are refinements eliminating real friction in common code patterns. Granular closure capture and the async prelude are the changes most appreciated in daily work with async code, which is half the Rust written today.
The migration is small enough that there’s no reason not to do it in active projects already using edition 2021. Risk is low, the tool does most of the work, and benefits are immediate. The only real blocker is if the project maintains compatibility with Rust versions before 1.85, in which case you wait or do the migration conditionally.
This article is also available in Spanish: Rust Edición 2024: lo que cambia de verdad en el día a día.
Sources:
- Rust Blog — Announcing Rust 1.85.0 and the 2024 Edition[1]
- Official Edition Guide — Rust 2024[2]
- GitHub — rust-lang/rust release notes (RELEASES.md)[3]
Frequently asked questions
Can I mix edition 2021 and 2024 crates in the same workspace?
Yes, without issues. A crate with edition = "2024" compiles against the same compiler as one with edition = "2021", and the two can depend on each other in either direction. The compiler knows each crate's edition and applies the matching semantics, so there is never cross-crate breakage. That lets you migrate a workspace crate by crate instead of all at once.
How long does migrating a real project to edition 2024 take?
For a typical application or library project without FFI, under thirty minutes. Update the toolchain to 1.85 or higher with rustup update stable, run cargo fix --edition and change edition = "2024" in the Cargo.toml files. Then run cargo test and cargo clippy and review whatever the tool could not migrate. With lots of FFI code or unusual closure patterns that manual review may take a few hours; the only real blocker is having to support Rust versions before 1.85.
What happens to my extern "C" blocks when moving to Rust 2024?
They now require the explicit unsafe keyword: unsafe extern "C" { ... }. You do not need to touch them by hand, because cargo fix --edition migrates them automatically. In return you get a new tool: inside an unsafe extern block you can mark functions you can guarantee are safe to call with safe fn, so they are called without unsafe. Those marked unsafe fn still require it at the call site.