Rust Editions
A Rust Edition (2015, 2018, 2021, 2024) is an opt-in milestone configured per crate in Cargo.toml that allows the Rust language to introduce new keywords, refine grammar rules, and update default preludes without ever breaking existing code written for earlier editions.
Why Editions Exist: Stability Without Stagnation
In most programming languages, adding a new keyword (such as async, await, or gen) creates an unavoidable dilemma: any existing program that used async or gen as a variable or function name immediately stops compiling when the compiler updates.
Rust solves this by separating compiler versions (1.0 through 1.85+, released every six weeks) from language editions (released roughly every three years):
- A crate declares which edition it uses via
edition = "2024"inCargo.toml. - New keywords, syntax changes, and prelude additions that could break older code are enabled only for crates that opt into the new edition.
- A single modern
rustccompiler supports all editions simultaneously.
Seamless Cross-Edition Interoperability
Unlike Python 2 vs Python 3—where libraries written in different versions could not easily be imported into the same program—Rust editions affect only how surface syntax is parsed and lowered to HIR. Inside rustc, every edition compiles down to the exact same Mid-Level IR (MIR) and binary ABI:
Crate A (edition = "2015") ──┐
Crate B (edition = "2018") ──┼──> Common HIR / MIR inside `rustc` ──> Single Linked Binary
Crate C (edition = "2021") ──┤
Your App (edition = "2024") ─┘
Your application running on the 2024 Edition can depend on crates from crates.io written in the 2015, 2018, 2021, or 2024 editions without any friction or wrapper code.
Timeline of Rust Editions
| Edition | Stabilized In | Key Changes Introduced |
|---|---|---|
| Rust 2015 | Rust 1.0 (May 2015) |
Baseline stable language; default edition when running rustc directly without --edition |
| Rust 2018 | Rust 1.31 (Dec 2018) |
Reserved async and await keywords; simplified module system (no extern crate needed); Non-Lexical Lifetimes (NLL); dyn Trait syntax |
| Rust 2021 | Rust 1.56 (Oct 2021) |
Disjoint field capture in closures (|| self.x); [a, b, c].into_iter() iterates by value; TryFrom/TryInto/FromIterator added to prelude; or_patterns in macros |
| Rust 2024 | Rust 1.85 (Feb 2025) |
Future and IntoFuture added to prelude; RPIT (-> impl Trait) captures all in-scope lifetimes by default (+ use<...> syntax); refined temporary drop order in tail expressions and if let; unsafe extern blocks and unsafe(no_mangle) attributes; reserved gen keyword |
Highlights of the Rust 2024 Edition
Setting edition = "2024" in Cargo.toml enables the latest language ergonomics and safety refinements:
1. Future and IntoFuture in the Prelude
You can write functions returning impl Future<Output = T> directly without importing std::future::Future:
fn compute_async() -> impl Future<Output = u32> {
async { 10 + 32 }
}
fn main() {
let _future = compute_async();
println!("Created Future using 2024 prelude");
}
Created Future using 2024 prelude
2. Explicit unsafe extern Blocks
In the 2024 edition, declaring foreign C functions requires writing unsafe extern "C" { ... }, making it explicit that declaring a foreign function signature is a promise to the compiler that the C signature is accurate:
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
let val = unsafe { abs(-42) };
println!("abs(-42) = {val}");
}
abs(-42) = 42
3. gen Reserved as a Keyword
To prepare for future generator blocks (gen { yield 1; }), gen is a reserved keyword in the 2024 edition. If you need to call a function named gen from an older library, use a raw identifier (r#gen).
Migrating a Project to a New Edition (cargo fix --edition)
Upgrading an existing crate from an older edition (such as 2021) to 2024 is automated by Cargo:
# 1. Automatically rewrite any syntax that changes meaning in the new edition:
cargo fix --edition
# 2. Update `Cargo.toml` to `edition = "2024"`
# 3. Verify the project compiles and passes tests:
cargo test
cargo fix --edition inspects your code and applies mechanical updates (such as converting extern "C" to unsafe extern "C" or escaping gen identifiers as r#gen) so your code's behavior remains identical under the new edition.