Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Rust uses 'editions' to evolve syntax without breaking old code — your Cargo.toml says edition = '2021' and that's the language version your code uses. Different crates in the same build can be on different editions; they interoperate. Editions are how Rust got ? for error propagation, async/await, the new let-else, etc., without splitting the ecosystem the way Python 2/3 did. Knowing which edition introduced which feature saves you from error: this requires edition 2018+.
Rust editions (2015, 2018, 2021, 2024) are opt-in language milestones that let the compiler introduce breaking syntax changes without breaking existing crates — each crate in your dependency graph can compile under a different edition simultaneously. When you bump the edition in Cargo.toml, cargo fix --edition automatically rewrites the code patterns that changed, making the migration largely mechanical. Understanding editions prevents confusion when reading code written before the current edition's idioms became standard.
edition line in your Cargo.toml. If it's 2018 or older, run cargo fix --edition to migrate.async fn foo() in a 2015-edition crate — it errors with a clear 'requires edition 2018+'. Now you've seen the message in the wild.cargo tree --edges all and find a dep on a different edition than yours. They still work together — that's the whole point.Use these three in order. Each builds on the one before.
In one paragraph, explain the Rust edition system — why isn't this just versioning the language?
When two crates on different editions are in the same build, what does the compiler actually do at the boundary? How can `?` work in one and not the other?
I maintain a library and want to bump from 2018 to 2021. What's the actual playbook — `cargo fix`, MSRV bump, dep audit, semver implications?
# what edition is this crate using?
$ grep edition Cargo.toml
edition = "2021"
# milestone features per edition (high level):
# 2015 — initial
# 2018 — module path changes ("crate::"), `async fn`, dyn-trait clarity
# 2021 — disjoint closure capture, `IntoIterator` for arrays, panic_unwind macros
# 2024 — async-fn-in-traits stabilized, RPITIT, Rust 1.85+ lifetime captures
# migrate to a new edition automatically:
$ cargo fix --edition
$ # then bump Cargo.toml edition manually after `cargo fix` is clean
# different crates can be on different editions:
$ cargo tree | head
myapp v0.1.0 (./) edition: 2021
└── serde v1.0.196 edition: 2018
└── serde_derive ... edition: 2015cargo run