Editions & What Changed in 2024
Rust promises that code which compiles today will keep compiling with future compilers. Yet languages need to fix old mistakes, add keywords and tidy rules, which could break existing code. Rust solves this conflict with editions: opt-in sets of changes that each project chooses for itself.
In this lesson you will learn how editions work, what each edition changed (so you can read older code), what the 2024 edition brought, how to migrate a project, and which features arrived in the regular releases since then, up to Rust 1.99.
How editions work
The edition is one line in Cargo.toml:
[package]
name = "marks_report"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"- New projects created with
cargo newuse the newest edition. A project without aneditionkey is treated as 2015. - Every Rust compiler supports every edition. Rust 1.99 compiles 2015 code exactly as it always did.
- Crates of different editions work together in one program. Your 2024 crate can depend on a library written for 2018, so the ecosystem never splits.
- Editions change only what code looks like to the compiler (keywords, some defaults, lints). After compilation, all editions produce the same kind of program.
rust-version(optional) states the oldest compiler your project supports.
A new edition has come out roughly every three years:
| Edition | Arrived with | Main changes |
|---|---|---|
| 2015 | Rust 1.0 (May 2015) | The original language |
| 2018 | Rust 1.31 (December 2018) | New module paths (crate::, no extern crate), dyn Trait, async and await reserved as keywords, non-lexical lifetimes |
| 2021 | Rust 1.56 (October 2021) | Closures capture individual fields, array.into_iter() yields values, TryFrom/TryInto/FromIterator in the prelude, Cargo feature resolver 2 by default |
| 2024 | Rust 1.85 (February 2025) | Changes listed below; the biggest edition so far |
Many features are not tied to editions at all. ?, async fn, let ... else, generic associated types and most library additions work in every edition, because they did not break existing code. Only changes that could break code wait for a new edition.
What the 2024 edition changed
Language
impl Traitcaptures lifetimes. A return type-> impl Traitnow automatically captures all generic parameters and lifetimes in scope. When you need to capture less, the precise-capturing syntaxuse<...>lists exactly what is captured.- Shorter temporary lifetimes. Temporaries in the condition of an
if let, and in the final expression of a block, are now dropped earlier. This removes a classic source of deadlocks and "already borrowed" panics. - Unsafe made explicit.
externblocks must be writtenunsafe extern, attributes such asno_manglebecome#[unsafe(no_mangle)], unsafe operations inside anunsafe fnneed their ownunsafe { }block, and references tostatic mutare denied (see Unsafe Rust). - Never type fallback changed, and some match-pattern combinations were reserved for future improvements to "match ergonomics".
- Macros: the
exprfragment specifier now also matchesconst { ... }blocks and_; a missing fragment specifier is now an error. - Reserved syntax:
genis a keyword (for future generator blocks), and guarded strings such as#"text"#are reserved. - Let chains (
if let ... && condition), stabilised later in Rust 1.88, are available only in the 2024 edition.
Standard library
FutureandIntoFutureare in the prelude.Box<[T]>implementsIntoIteratorby value.std::env::set_varandremove_varare nowunsafeto call, because changing environment variables while other threads read them is a data race.
Cargo, Rustdoc and Rustfmt
- The dependency resolver is rust-version aware (resolver 3): it prefers dependency versions that support your
rust-version. - Old spellings in
Cargo.toml, such asdev_dependencieswith an underscore, are no longer accepted; usedev-dependencies. - Doc tests are combined into one program per crate, which runs much faster.
- Rustfmt has a separate style edition, and sorts names containing numbers in a more natural "version" order.
Two changes you can see in code
The first example shows use<> asking a returned iterator to capture nothing. Without it, in 2024 the iterator would be treated as borrowing names, and clearing the list would be rejected:
fn lengths(names: &[String]) -> impl Iterator<Item = usize> + use<> {
let v: Vec<usize> = names.iter().map(|n| n.len()).collect();
v.into_iter()
}
fn main() {
let mut names = vec![String::from("Isha"), String::from("Rohan")];
let lens = lengths(&names);
names.clear(); // allowed: `lens` does not borrow `names`
println!("{:?}", lens.collect::<Vec<_>>());
}[4, 5]
The second shows the new if let temporary scope. The cache.borrow() guard is now dropped before the else block runs, so borrowing mutably there is fine. Under the 2021 edition the guard lived until the end of the whole if let ... else, and this program would panic with an "already borrowed" error:
use std::cell::RefCell;
fn main() {
let cache: RefCell<Option<u32>> = RefCell::new(None);
if let Some(v) = *cache.borrow() {
println!("Cached value: {v}");
} else {
*cache.borrow_mut() = Some(42);
println!("Stored 42");
}
println!("{:?}", cache.borrow());
}Stored 42 Some(42)
Migrating a project to 2024
Cargo can rewrite most code automatically. The usual steps are:
rustup update stable
cargo fix --edition # rewrites code so it works in both editions
# now change edition = "2021" to edition = "2024" in Cargo.toml
cargo build
cargo testcargo fix --edition is deliberately cautious: it keeps the old behaviour exactly, sometimes by making code more verbose (for example adding use<..> bounds or extra blocks). After the switch, review its changes and simplify where the new behaviour is what you wanted. In a workspace with many crates, you can migrate one crate at a time.
Release highlights since the 2024 edition
Between editions, a new stable Rust appears every six weeks. A selection of what arrived after Rust 1.85:
| Version | Date | Highlights |
|---|---|---|
| 1.86 | April 2025 | Trait upcasting (&dyn Child to &dyn Parent), get_disjoint_mut on slices and HashMap |
| 1.87 | May 2025 | Vec::extract_if, anonymous pipes in std::io::pipe |
| 1.88 | June 2025 | Let chains (2024 edition), naked functions, Cargo cleans its cache automatically |
| 1.90 | September 2025 | LLD is the default linker on x86-64 Linux, cargo publish --workspace |
| 1.94 | March 2026 | array_windows on slices, include in Cargo config, TOML 1.1 in manifests |
| 1.95 | April 2026 | if let guards in match arms, the cfg_select! macro |
| 1.96 | May 2026 | New range types in core::range, the assert_matches! macro |
| 1.97 | July 2026 | CARGO_BUILD_WARNINGS to allow or deny warnings, v0 symbol mangling by default |
| 1.98 | August 2026 | Algebraic float methods such as algebraic_add, format_into for integers |
| 1.99 | October 2026 | Defining C-variadic functions in Rust, Vec::into_parts and Vec::from_parts |
The official release notes on the Rust blog list every change. A good habit is to run rustup update when a release appears and skim its announcement.
Common mistakes
- Changing the edition number before running
cargo fix --edition. Run the fix first, while the old edition is still set, then change the number. - Thinking an old edition means an old compiler. You can use Rust 1.99 with edition 2018; you simply do not get the 2024 rules.
- Expecting every new feature to need a new edition. Most features arrive in normal six-weekly releases for all editions.
- Assuming
cargo fixoutput is the ideal style. It preserves behaviour; tidy it afterwards.
Key points
- An edition (2015, 2018, 2021, 2024) is chosen per crate in
Cargo.toml; crates of different editions work together. - Editions contain only changes that could break code; most features ship in regular releases every six weeks.
- The 2024 edition changed
impl Traitlifetime capture, temporary scopes, unsafe rules, the prelude and Cargo's resolver, and enables let chains. - Migrate with
cargo fix --edition, then setedition = "2024"and build and test. - Releases after 1.85 added features such as trait upcasting, let chains,
if letguards andassert_matches!; Rust 1.99 is the version this course teaches.
Exercise
Look at this 2015-style snippet and rewrite it in modern 2024-edition Rust: extern crate serde;, a function fn show(item: &Display) { println!("{}", item); }, and a nested if let Some(n) = maybe { if n > 10 { ... } }. Then list the three commands you would run to migrate an old 2021 project to 2024.
Show solution
use std::fmt::Display;
fn show(item: &dyn Display) {
println!("{item}");
}
fn main() {
show(&"Kochi");
let maybe: Option<u32> = Some(25);
if let Some(n) = maybe
&& n > 10
{
println!("Big number: {n}");
}
}Kochi Big number: 25
extern crate is no longer needed: dependencies listed in Cargo.toml are available by name. Trait objects need dyn, and the nested if let becomes a let chain. To migrate: cargo fix --edition, then edit Cargo.toml to edition = "2024", then cargo build and cargo test.
Quick Quiz
1. Can a crate using the 2024 edition depend on a library written for the 2018 edition?
Editions are per crate, and the compiler handles each crate with its own edition rules.
2. Which command rewrites code so it can move to a new edition?
cargo fix --edition applies automatic, behaviour-preserving migrations before you change the edition number.
3. Which feature works only in the 2024 edition?
Let chains depend on the 2024 edition's temporary scope rules; the others work in all editions.
4. What does the use<> in -> impl Iterator<Item = usize> + use<> mean?
Precise capturing lists what the hidden type may borrow; an empty list captures nothing.
Learn Rust with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
