---
title: "B.1 Rust editions in detail"
description: "What each Rust edition actually changed, from the 2015 baseline through 2018, 2021, and 2024, and why none of it broke older code."
url: "https://learnrust.net/appendix-b/rust-editions-in-detail/"
last_updated: "2026-06-13"
---

# B.1 Rust editions in detail

Lesson [0.12](https://learnrust.net/chapter-0/rust-editions/) introduced **editions**: the mechanism that lets Rust make occasional breaking changes to its surface syntax without breaking the code already written. Each crate declares its edition in `Cargo.toml`, the compiler holds every crate to the rules of its own edition, and crates of different editions link together freely. That lesson promised a catalog of what the editions actually changed, deferred to here because most of it only makes sense once you know the language. Now you do.

This is a tour, not the full specification. Each edition's complete change list lives in the official [Rust Edition Guide](https://doc.rust-lang.org/edition-guide/), which also documents the automatic migration (`cargo fix --edition`) for each one. What follows is the highlights, the changes you're most likely to notice or to read about.

> **Editions are not language versions**
>
> Worth restating before the list: there's one Rust compiler, always current, and it understands all editions. A new compiler release brings new features (new standard library functions, say) to *every* edition at once. An edition boundary is reserved for the rare change that would alter the meaning of code that already compiles. So almost everything Rust has added over the years works regardless of your edition; only the items below were edition-gated.

## Rust 2015: the baseline

The 2015 edition is Rust as it stood at the 1.0 release, the language's first stable form. There's nothing to "change" here; it's the starting point every later edition is measured against, and it still compiles unchanged today. A few things look dated in 2015 code if you come across it. Dependencies had to be pulled into scope with an explicit `extern crate serde;` line at the top of the crate. Trait objects were written as a bare trait name (`Box<Error>`) rather than with today's `dyn` keyword. And the module system had some rough corners around how paths in `use` statements were resolved, which is exactly what the next edition set out to fix.

## Rust 2018: the module system, mostly

Shipped with Rust 1.31 in late 2018, this was the largest edition change, and most of it was about modules and paths, the material of chapter 13. If you learned modules in this course, you learned the 2018 rules, so these will read as "the way it works" rather than "the change."

The headline was **path clarity**. The `extern crate` line went away: a dependency listed in `Cargo.toml` is simply usable, and paths in `use` statements now start unambiguously from a crate name, or from `crate::` for your own crate root, `self::`, and `super::` (lesson [13.1](https://learnrust.net/chapter-13/naming-collisions-and-modules/)). The old requirement that every module directory contain a `mod.rs` file was relaxed too, so you can write `foo.rs` beside a `foo/` directory instead (lesson [13.4](https://learnrust.net/chapter-13/splitting-code-into-files/)).

Alongside the module work, 2018 reserved the `async` and `await` keywords, clearing the way for `async`/`await` to stabilize shortly after in compiler version 1.39 (chapter 23). It also made `dyn Trait` the standard way to write a trait object (chapter 16), with the bare-trait-name form deprecated. And the borrow checker quietly got much smarter around this time with **non-lexical lifetimes**, which let a borrow end as soon as it's last used rather than at the end of its block, accepting a lot of obviously-correct code the old checker rejected. You've been relying on that the whole course without knowing it had a name.

## Rust 2021: smaller, sharper

Rust 1.56, in late 2021, brought a tighter set of changes. The one most likely to affect everyday code was **disjoint closure captures**. Before 2021, a closure that used `point.x` captured the entire `point`; from 2021 on it captures just the field `point.x`, which means the rest of `point` stays available to other code. This removes a class of borrow-checker annoyances around closures (chapter 19) where you previously had to manually destructure a struct to satisfy the compiler.

Two more changes you've benefited from directly. Arrays became iterable by value: `for x in [1, 2, 3]` now hands you each value, where before it gave references (a consequence of arrays implementing `IntoIterator`, lesson [19.6](https://learnrust.net/chapter-19/for-loops-demystified/)). And the **prelude** (the set of names available without importing, lesson [13.1](https://learnrust.net/chapter-13/naming-collisions-and-modules/)) gained `TryFrom`, `TryInto`, and `FromIterator`, so the fallible conversions from chapter 16 work without a `use` line.

The 2021 edition also made the `panic!` macro consistent with `println!`, so `panic!("value: {}", x)` formats its arguments the way you'd expect, and reserved some new syntax for the future, including the `prefix#`, `prefix"..."`, and `prefix'...'` forms. That reservation is what later allowed C string literals, written `c"..."`, to exist.

## Rust 2024: tightening the unsafe edges

The newest edition shipped with Rust 1.85 in early 2025. Its theme is precision: several changes tighten defaults that were a little too loose, especially around `unsafe` code (chapter 25) and the exact moment temporary values are dropped.

On the `unsafe` side, external function declarations now go in an `unsafe extern` block rather than a plain `extern` block, making the unsafety visible at the declaration (chapter 25). Certain attributes that affect linking, such as `#[no_mangle]`, must now be written as `#[unsafe(no_mangle)]`, since getting them wrong can break safety. And `std::env::set_var` and `remove_var` became `unsafe` functions, because changing environment variables while other threads are running can cause real undefined behavior (lesson [20.4](https://learnrust.net/chapter-20/environment-variables/)). References to a `static mut` are also now disallowed by default, steering you toward the safer interior-mutability tools from chapter 21.

The other cluster of changes is about **temporary scopes**, the precise point at which a short-lived value gets dropped. The rules for temporaries created in an `if let` scrutinee and in a function's tail expression were adjusted so that things are dropped sooner and more predictably, which occasionally changes the order destructors run in. It's the kind of refinement you'll almost never notice but that closes some genuine footguns. Rounding out the edition: the return-position `impl Trait` lifetime-capture rules were made more uniform, the `gen` keyword was reserved for a future generators feature, and on the tooling side Cargo gained a dependency resolver that pays attention to each crate's declared minimum Rust version when choosing dependency versions.

## What you actually do about it

Nothing changes the advice from lesson 0.12: start new projects on the latest edition, which `cargo new` does for you, and leave the `edition` line alone until you deliberately migrate. When you do migrate an older project, `cargo fix --edition` rewrites the mechanical parts for you and the compiler flags anything it can't handle automatically. The reason this appendix can exist at all, a clean per-edition list rather than a migration horror story, is the whole point of the design: Rust gets to keep improving its surface syntax, and your old code keeps compiling regardless.

The final appendix is shorter and not technical at all. It's about where you go from here.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
