You installed Rust 1.78, but the tutorial says 2021
You run rustc --version and see 1.78.0. You open a tutorial and it tells you to set edition = "2021" in your Cargo.toml. You pause. Is the tutorial outdated? Do you need a special "2021" version of the compiler? The answer is no, and yes, but not in the way you expect.
Rust editions are not compiler versions. They are snapshots of the language grammar and standard library rules. The compiler version is the tool that enforces those rules. One compiler can enforce multiple editions. rustc 1.78.0 can compile code written for the 2015, 2018, 2021, and 2024 editions. The edition tells the compiler which rulebook to use for your specific crate.
The building code analogy
Think of Rust editions like building codes for a city. In 2015, the city adopted a code. Every house built since then follows those rules. In 2018, the city updates the code. New houses must follow the 2018 rules. The 2015 houses are still standing. They are legal. The inspectors get updated to check both 2015 and 2018 rules. You don't demolish the old houses. You just mark them as "built to 2015 code".
Rust works the same way. The compiler is the inspector. The edition is the code version. When you write a new project, you choose which edition to follow. When you update the compiler, you get a better inspector that knows the latest rules, but it can still inspect old projects using the old rules. This separation lets the language evolve without breaking existing code.
How you set the edition
You specify the edition in your Cargo.toml file under the [package] section. This is the only place that matters for your crate.
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"
The edition key tells the compiler which language rules apply to this crate. If you omit the key, Cargo uses a default. The default changes over time. New versions of Cargo default to newer editions. Relying on the default is a recipe for drift. Pin the edition explicitly.
Convention is to pin the edition in Cargo.toml. This ensures every developer and CI runner uses the same rules, regardless of their local Cargo version. Treat the edition field as part of your project's contract.
What changes between editions
Editions introduce changes to syntax, module resolution, trait behavior, and standard library APIs. The changes are curated to improve the language while keeping migration automated where possible.
Edition 2015
Edition 2015 is the original. It requires extern crate declarations to bring dependencies into scope. Module resolution is path-based. If you have a file src/lib.rs and a file src/foo.rs, you access foo as a module. If you have src/foo/mod.rs, you access it the same way. The rules are simple but rigid.
// 2015 style
extern crate serde;
use serde::Serialize;
fn main() {
println!("Hello, 2015!");
}
The extern crate line is mandatory in 2015. Without it, the compiler cannot find the dependency.
Edition 2018
Edition 2018 removes the need for extern crate. The compiler automatically brings dependencies from Cargo.toml into scope. If you write extern crate serde; in 2018, the compiler warns you. It's redundant.
Module resolution changes significantly. The compiler now looks for modules relative to the crate root, not the current file. This makes large projects easier to organize. You can put modules in subdirectories without mod.rs files.
// 2018+ style
// extern crate is gone. The compiler finds serde automatically.
use serde::Serialize;
fn main() {
println!("Hello, 2018!");
}
Edition 2018 also improves the ? operator. It now works with more types. Disjoint capture in closures becomes available, allowing closures to borrow parts of a struct without capturing the whole thing.
Edition 2021
Edition 2021 stabilizes async and await. This is the biggest feature. You can write asynchronous code without external crates like futures. The syntax is clean and integrated into the language.
// 2021+ style
async fn fetch_data() -> String {
// Simulate async work
"data".to_string()
}
#[tokio::main]
async fn main() {
let data = fetch_data().await;
println!("Got: {}", data);
}
Edition 2021 also improves IntoIterator for references. You can iterate over a slice by reference without extra syntax. The try_trait_v2 allows the ? operator to work with more error types.
Edition 2024
Edition 2024 is the latest edition. It introduces extern type, allowing you to declare types that are defined in foreign code. This is useful for FFI and low-level systems programming. It also improves const trait bounds and adds unsafe trait bounds. These features give you more control over unsafe code and generic constraints.
// 2024+ style
extern type OpaqueType;
fn process(opaque: *mut OpaqueType) {
// Work with opaque types from C
}
Edition 2024 also refines module resolution and trait behavior. The changes are incremental but powerful. Use 2024 if you need the newest features and are comfortable with the latest compiler toolchain.
The migration path
You don't need to rewrite your code to switch editions. Rust provides an automated migration tool. Change the edition in Cargo.toml and run cargo fix --edition. The tool rewrites your source code to match the new rules.
# Update the edition in Cargo.toml
# Then run the automated fixer
cargo fix --edition
The fixer handles mechanical changes like removing extern crate and updating module paths. It leaves semantic changes to you. Review the changes. The tool is smart, but you are smarter.
Convention is to run cargo fix --edition immediately after changing the edition. Commit the changes. This keeps your codebase clean and up-to-date.
Pitfalls and compiler errors
Mixed editions
Your crate can use a different edition than its dependencies. This is fine. The compiler handles the boundary. If your crate is 2021 and a dependency is 2015, the compiler compiles the dependency with 2015 rules and your crate with 2021 rules. You don't need to update dependencies to match your edition.
Default edition drift
If you omit the edition key, Cargo uses a default. The default changes when you update Cargo. A project that defaults to 2018 today might default to 2021 tomorrow. This causes subtle bugs. Code that compiles on one machine might fail on another.
Pin the edition. Always.
Module resolution errors
If you switch from 2015 to 2018, module resolution changes. You might get a "cannot find module" error. The compiler looks for modules in a different place.
error[E0432]: unresolved import `foo`
--> src/lib.rs:1:5
|
1 | use foo::bar;
| ^^^ maybe a missing crate `foo`?
The error E0432 means the compiler can't find the module. Check your file structure. In 2018, modules are resolved relative to the crate root. Move your files or update your use statements.
Feature gate errors
If you use a feature that requires a newer edition, the compiler tells you exactly which edition you need.
error[E0658]: `async` functions are not stable in edition 2018
--> src/main.rs:1:1
|
1 | async fn main() {}
| ^^^^^^^^^^^^^^^^^^
|
= note: this feature requires edition 2021 or later
The error E0658 means you're using an unstable feature or a feature that requires a newer edition. Update your edition or remove the feature.
Decision matrix
Use edition 2021 for new projects. It's the current stable default and includes async/await and modern module resolution.
Use edition 2024 when you need the latest language improvements like extern type or const trait bounds, and you are comfortable with the newest compiler toolchain.
Use edition 2018 only if you are maintaining legacy code that predates 2021 and haven't migrated yet.
Use edition 2015 only for historical analysis or if you are stuck on a compiler version older than 1.31.
Pin your edition. Defaults drift.