🎓 A1Learner
🔍
0 of 149 lessons completed

Rust Editions

An edition is a bundle of language changes that a crate opts into. Rust promises that old code keeps compiling.

One line opts in

An edition is a package of language changes, and your crate opts into it with one line in Cargo.toml. Leave the line out, and the compiler uses 2015 — the promise that the oldest code still builds:

# Cargo.toml
[package]
name = "my_crate"
version = "0.1.0"
edition = "2021"

A raw identifier bridges the gap

A new keyword is the classic edition change. try became reserved in 2018. Only crates that opt in lose it as a name. And a raw identifier lets you use it anyway:

let r#try = 5;
println!("try = {}", r#try);

Move a crate with cargo fix

Cargo rewrites what it safely can. You then raise the edition line, and fix the few leftovers by hand — the Edition Guide lists every change:

// in your terminal:
// $ cargo fix --edition

One file by hand follows 2015 too

rustc on a single file uses the oldest rules until you pass the edition as a flag:

// $ rustc main.rs  (2015 rules)
// $ rustc --edition 2021 main.rs  (2021 rules)

2021: conversions joined the prelude

try_into used to need an import line. Since 2021 it is in the prelude — always in scope. In a 2018 crate the same line fails: the compiler answers that no method named try_into exists.

let big: i32 = 300;
let small: u8 = big.try_into().unwrap_or(255);

2021: closures capture single fields

Before 2021 a closure captured the whole variable, so using one field after moving another was refused. Since 2021 the closure below captures only pair.b, and the program runs.

drop(pair.a);
let show_b = || println!("b = {}", pair.b);
show_b();

2021: arrays iterate by value

Arrays joined the iterator family in 2021. Before that, into_iter() on an array quietly handed you references. Collecting numbers into a Vec failed with a type error.

let nums: Vec<i32> = [10, 20, 30].into_iter().collect();

2021: panic! demands a message

panic!(42) compiled in 2018 with only a warning — the number became a hidden payload. Since 2021 it is an error: a panic message must be text, written like panic!("went wrong").

panic!(42); // 2018: warning. 2021: error

2024: gen is saved for generators

Rust 2024 reserves the word gen for generator blocks that are coming. A variable named gen is fine in 2021 and an error in 2024 — write r#gen, or pick another name.

let gen = 3; // fine in 2021, an error in 2024

2024: extern blocks say unsafe out loud

Calling C code was always unsafe work. Since 2024 the block itself must say so: unsafe extern. In 2021, the same block without unsafe compiles. In 2024, the compiler refuses it.

unsafe extern "C" {
    fn abs(x: i32) -> i32;
}

Three 2021 changes in one program

In this example, three 2021 changes work together. try_into needs no import. 2021 put it in the prelude. 300 does not fit in a u8. So unwrap_or keeps 255. The array [10, 20, 30] turns into a Vec of numbers because 2021 arrays iterate by value. And the closure prints pair.b even though pair.a was dropped, because 2021 closures capture single fields. Compile this same file as edition 2018 and all three lines fail — same compiler, same code, different rulebook.

struct Pair { a: String, b: String }
fn main() {
    let big: i32 = 300;
    let small: u8 = big.try_into().unwrap_or(255);
    let nums: Vec<i32> = [10, 20, 30].into_iter().collect();
    let pair = Pair { a: String::from("first"), b: String::from("second") };
    drop(pair.a);
    let show_b = || println!("b = {}", pair.b);
    show_b();
    println!("small = {}, nums = {:?}", small, nums);
}

Example

fn main() {
    println!("2015: the first edition - the original rules");
    println!("2018: clearer module paths, one shared style");
    println!("2021: closures capture single fields, arrays join iterators");
    println!("2024: let chains, and unsafe code is marked more clearly");
    println!("editions are opt-in: old code keeps compiling");
}

Key points

  • An edition is a package of language changes your crate opts into with one line: edition = "2021" in Cargo.toml
  • Rust itself ships small changes every six weeks
  • An edition gathers about three years of them into one clear step
  • Opting in is per crate
  • One compiler builds every edition
  • Crates of different editions can depend on each other freely
  • An edition changes how code is first read
  • It does not change the library it links to
  • You are warned before you must move
  • Migration lints flag code that the next edition would reject
  • That code still compiles in your edition
  • So upgrading rarely surprises
  • Your crate picks its edition once, in Cargo.toml — the snippets below name the edition each line belongs to
  • An edition never changes what correct code means
  • It changes which names, captures and shortcuts the compiler allows
  • When code from a tutorial fails in your project, check the edition first
  • A surprising share of "the tutorial is wrong" moments are edition differences
  • Every refusal here was reproduced with the real compiler.

In this example, the program prints the four editions as a timeline. 2015 is the original rulebook every crate starts from. Each later line names one or two changes that edition is known for. The last line states the deal that makes editions safe: they are opt-in. A crate keeps its old edition until its owner edits one line in Cargo.toml. So upgrading the compiler never breaks existing code.

Note

An edition is not a fork like Python 2 and 3

There is one Rust and one standard library

An edition is one setting in one file

Nothing here is a trick to memorise

It is a reading skill

When an error mentions a keyword, a capture or a missing method, ask one question

Ask: "is this my edition talking?"

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

What happens to a 2015 crate when a new edition of Rust appears?