View : 124

08/11/2026 10:00am

Cover for Rust 2024 Edition in 2026 showing two orange blocks and a gear on a dark blue background, with chips for Rust 1.85, let chains and resolver 3

Rust 2024 Edition in 2026: What Changed, How to Migrate, and the let chains Bonus

#Rust

#Rust Programming

#Rust Language

#Rust 2024 Edition

#cargo fix

#Rust 1.85

Picture this: you reopen a Rust project you left alone last year and see edition = "2021" in Cargo.toml, while every announcement and doc page talks about 2024. The first questions that come to mind are usually: what happens if I do not move, and if I do, what breaks?

The short answer is that a project on an older edition is not harmed, and most of the migration is automated. But a few spots cannot be fixed by tooling, and a few change behavior quietly, so they are worth knowing in advance. Today Superdev Academy walks through what Rust 2024 Edition changes in real code, how to migrate, and what you must always check yourself. We rely mainly on the Rust 1.85.0 announcement and the Rust 2024 Edition Guide.

We checked this against the official sources again in early November 2026. By then Rust 2024 had been out for about 20 months, and the latest stable we found on the official blog was Rust 1.99.0 (1 October 2026), followed by 1.99.1 on 15 October 2026. The list of edition changes is the same as at release, but the context is different: 2024 is now the current edition, and a feature that arrived after 1.85 is available only on it, so we added a let chains section below.

What is an edition, and which version shipped 2024?

An edition is how Rust ships changes that are not backward compatible without forcing everyone to update at once. Each crate picks its own edition through the edition key in Cargo.toml. If the key is missing, Cargo treats the crate as Rust 2015.

Rust 2024 was announced as stable together with Rust 1.85.0 on 20 February 2025. The announcement calls it the largest edition released so far and tells you to update with rustup update stable. Shipping changes in steps through editions is how the community keeps the language stable while it keeps evolving.

Overview of the changes in Rust 2024

The 1.85.0 announcement groups the changes by area. On the language side: RPIT lifetime capture rules, if let temporary scope, tail expression temporary scope, match ergonomics reservations, unsafe extern blocks, unsafe attributes, the unsafe_op_in_unsafe_fn warning, disallowing references to static mut, the never type fallback change, macro fragment specifier changes, the gen keyword, and reserved syntax.

In the standard library: the prelude gains Future and IntoFuture, IntoIterator for Box<[T]>, and the newly unsafe functions std::env::set_var, std::env::remove_var and CommandExt::before_exec. In Cargo: a Rust-version aware resolver, consistent table and key names, and rejecting unused inherited default-features. There are also changes in rustdoc and rustfmt.

A list this long does not mean your code will break. Most items only touch code written in particular ways. Below are the ones most developers are likely to meet.

1. impl Trait in return position captures more lifetimes

This is one of the changes that can make the borrow checker report new errors in old code. In Rust 2024, when a function returns impl Trait (RPIT) without a use<..> bound, the returned type captures all in-scope generic parameters, including lifetimes. Earlier editions captured lifetimes implicitly only in certain positions. The Edition Guide shows the comparison:

fn f_implicit(_: &()) -> impl Sized {}
// In Rust 2021 and earlier, the above is equivalent to:
fn f_2021(_: &()) -> impl Sized + use<> {}
// In Rust 2024 and later, it's equivalent to:
fn f_2024(_: &()) -> impl Sized + use<'_> {}

The consequence is that the returned value may be tied to its arguments for longer than before, so caller code that used to pass can be rejected by the borrow checker. To state that the returned value holds on to no lifetime at all, use use<>. Precise capturing with use<..> has been stable since Rust 1.82, so it works in every edition.

fn capture<'a>(_: &'a ()) -> impl Sized + use<> {}

fn test<'a>(x: &'a ()) -> impl Sized + 'static {
    capture(x) //~ OK
}

The helpers are the impl_trait_overcaptures lint, part of the rust-2024-compatibility group, and cargo fix --edition, which can add use<..> in most cases. The exception the guide names is functions with impl Trait in argument position (APIT): such a parameter has no name to put in use<..>, so you must give the generic a name first. That step is manual.

2. if let drops temporaries before else

This subtle change directly prevents hard-to-find bugs such as deadlocks. In Rust 2024, temporaries created while evaluating the expression of an if let are dropped before the else block runs. In older editions they lived until the end of the whole if let/else. The guide's example holds a read lock on an RwLock:

use std::sync::RwLock;

fn f(value: &RwLock<Option<bool>>) {
    if let Some(x) = *value.read().unwrap() {
        println!("value is {x}");
    } else {
        let mut v = value.write().unwrap();
        if v.is_none() {
            *v = Some(true);
        }
    }
    // <--- Read lock is dropped here in 2021
}

In edition 2021 the read lock from value.read() is still held when the else block starts, so asking for the write lock there deadlocks. In 2024 the read lock is released before else, and the same code works. For migration, the if_let_rescope lint flags places where behavior changes, and cargo fix --edition rewrites if let into match, which keeps the 2021 temporary lifetime. The guide warns that in the deadlock example the match version still holds the lock in the _ arm, so it does not fix the problem. Review each rewrite and go back to if let wherever the 2024 behavior is what you want.

3. References to static mut are denied by default

The static_mut_refs lint becomes deny by default in 2024: you may not create shared or mutable references to a static mut. The guide explains why. Creating a reference that breaks the mutability XOR aliasing rule is undefined behavior the instant it is created, even if you never read or write through it, and upholding the rule for a static mut means reasoning about the whole program at once, which is very hard with reentrancy or multiple threads.

static mut X: i32 = 23;
static mut Y: i32 = 24;

unsafe {
    let y = &X;             // ERROR: shared reference to mutable static
    let ref x = X;          // ERROR: shared reference to mutable static
    let (x, y) = (&X, &Y);  // ERROR: shared reference to mutable static
}

There is no automatic fix; you rewrite the code yourself. The guide lists alternatives in rough order of preference: avoid globals if you can, use atomics for simple values, use Mutex or RwLock for more complex types, use LazyLock or OnceLock for one-time initialization that cannot be const, and if you truly must keep static mut, such as passing a pointer to a C library, use &raw mut or &raw const instead of creating a reference.

use std::sync::atomic::Ordering;
use std::sync::atomic::AtomicU64;

static COUNTER: AtomicU64 = AtomicU64::new(0);

fn main() {
    COUNTER.fetch_add(1, Ordering::Relaxed);
}

4. extern blocks need unsafe, and some functions become unsafe

In Rust 2024 an extern block must be written as unsafe extern, because the author of the block is responsible for the signatures being correct; if they are wrong, the result is undefined behavior. (Rust 1.82 already allows unsafe extern in every edition.) Each item in the block can be marked safe or unsafe, and an item with no qualifier is treated as unsafe.

unsafe extern "C" {
    // sqrt (from libm) may be called with any `f64`
    pub safe fn sqrt(x: f64) -> f64;

    // strlen (from libc) requires a valid pointer,
    // so we mark it as being an unsafe fn
    pub unsafe fn strlen(p: *const std::ffi::c_char) -> usize;

    // this function doesn't say safe or unsafe, so it defaults to unsafe
    pub fn free(p: *mut core::ffi::c_void);

    pub safe static IMPORTANT_BYTES: [u8; 256];
}

The missing_unsafe_on_extern lint adds the keyword automatically, but it cannot check that the signatures are right, so you must review them. The functions that become unsafe in 2024 are std::env::set_var, std::env::remove_var and CommandExt::before_exec. The first two are a problem because changing the process environment can be unsound in multi-threaded programs on some platforms, so you must not call them while another thread might be running. To avoid breaking existing code, they are unsafe only from edition 2024 on.

fn main() {
    // TODO: Audit that the environment access only happens in single-threaded code.
    unsafe { std::env::set_var("FOO", "123") };
}

The tool wraps the call in unsafe, but the guide stresses that it does not verify the usage is correct. You still have to confirm that no other thread can be running at the call. If you cannot guarantee that, consider a mechanism other than environment variables.

5. Cargo uses a Rust-version aware resolver

With edition = "2024", Cargo implies resolver = "3", a resolver that considers the package's rust-version when choosing dependency versions. (From Rust 1.84.0 this was opt-in through resolver.incompatible-rust-versions = "fallback" in .cargo/config.toml.)

# Cargo.toml
[package]
name = "my-crate"
edition = "2024"        # implies resolver = "3"
rust-version = "1.85"   # the resolver considers this when choosing dependency versions

Two cautions from the guide. First, the resolver is a workspace-wide setting and is honored only for the top-level package. Second, with a virtual workspace you must set resolver = "3" under [workspace] yourself if you want the new one. There is no automated migration for this; the guide suggests testing against the latest dependencies in CI to catch dependency bugs early.

A bonus after migrating: let chains only work on edition 2024

This is the practical reason to move to 2024 in 2026. The Rust 1.88.0 announcement (26 June 2025) brought let chains to stable, which lets you chain several let statements with && in the condition of an if or while without nesting multiple if let blocks. The announcement says the feature is available only on edition 2024 because it depends on the if let temporary scope change we described in item 2. The example from the announcement:

if let Channel::Stable(v) = release_info()
    && let Semver { major, minor, .. } = v
    && major == 1
    && minor == 88
{
    println!("`let_chains` was stabilized in this version");
}

In short, to write this you need edition = "2024" and a toolchain of 1.88.0 or later. As for the next edition, we searched and found no official announcement, only opinion pieces speculating about 2027, which we do not treat as confirmed, so we say nothing further about it.

New in 1.85.0 but not tied to the edition

The 1.85.0 announcement also covers async closures, written async || {}, which return futures and can borrow from their captures, along with the new AsyncFn, AsyncFnMut and AsyncFnOnce traits in the prelude. It also mentions the #[diagnostic::do_not_recommend] attribute, which hides a trait implementation from compiler suggestions, and FromIterator and Extend now covering tuples of 1 to 12 elements. For details we recommend the original announcement, because these items are not part of an edition migration.

Migrating a real project

The Edition Guide describes five steps (its example goes from 2015 to 2018, but the steps are the same for every edition). Applied to 2024: first run rustup update stable to get 1.85.0 or later (in 2026 that simply means the latest stable, and using let chains requires 1.88.0 or later), then:

cargo update
cargo fix --edition

Step one, cargo update, refreshes dependencies, since some crates, especially proc-macros and crates that generate code at build time, can have issues with a new edition and a newer release may already fix them. The guide suggests running your tests right away and committing separately if you use git. Step two, cargo fix --edition, checks the code and fixes what it can; anything it cannot fix prints a warning for you to handle by hand. Step three is editing Cargo.toml:

[package]
name = "foo"
version = "0.1.0"
edition = "2024"

Step four is cargo build and cargo test. If new warnings appear, run cargo fix again without --edition to apply the compiler's suggestions. Step five, optional, is cargo fmt to reformat with the new edition's style; commit it separately so git blame stays clean.

What the automatic tool does not do

The 1.85.0 announcement warns that cargo fix is conservative and preserves existing behavior, so its output is not a recommendation for every project. The migration guide also states that automatic migration does not update doctests, and code that uses macros or build-time code generation may need manual changes. Our pre-merge checklist: open the diff of every if let turned into match, check the signatures in every unsafe extern block, check every set_var and remove_var call really runs in single-threaded code, run doctests separately, and test against the latest dependencies in CI.

If your lifetime knowledge is not yet solid enough to read RPIT errors comfortably, review the Ownership rules and Borrowing first. And if you are still deciding whether to learn Rust at all, Why Rust? is a good place to start.


Summary: migrating an edition is not scary, but never accept the diff unread

Rust 2024 shipped with 1.85.0 and is the largest edition the Rust team has released, yet the changes that touch most developers are few: RPIT capturing more lifetimes, if let temporaries dropping earlier, the ban on references to static mut, unsafe extern and newly unsafe functions, and Cargo's resolver 3, with let chains since 1.88.0 as a bonus that works only on this edition. cargo fix --edition helps a lot, but it preserves old behavior; it does not decide for you what the code should be.

If you keep one thing, treat the output of cargo fix as a first draft to review, especially around locks, unsafe and environment variables, where mistakes do not crash immediately but surface later when the program is running for real.

🎯 Follow dev knowledge from Superdev Academy:

FAQ: Frequently Asked Questions about This Article

A collection of questions and answers to help you better understand the content of this article.