Rust Edition 2021 is a collection of backward-incompatible changes designed to improve the language and its tooling.
Let’s see this in action. Imagine you have a Rust project that was last compiled with the 2018 edition. To migrate, you’ll typically run:
cargo upgrade --edition
This command is smart. It analyzes your Cargo.toml and src/main.rs (or src/lib.rs) and attempts to automatically apply the necessary changes. For most projects, this is all you need. However, understanding what it’s doing and why certain things might break is crucial for more complex scenarios.
The core problem Edition 2021 solves is the gradual evolution of Rust’s syntax and features. Instead of a single, massive breaking change that would be impossible to adopt, editions allow for controlled evolution. Each edition bundles a set of changes that, while not affecting every project, can be significant for those using specific features. The goal is to make Rust code more robust, expressive, and easier to maintain over time without alienating existing users.
Here’s a look at some of the key changes and how they manifest:
1. IntoIterator for for loops
The Change: for x in y now requires y to implement IntoIterator directly. Previously, it would also work if y implemented IntoIterator for &y or &mut y.
Why it Matters: This change makes the behavior of for loops more consistent and predictable. It forces you to be explicit about whether you’re iterating by value, by reference, or by mutable reference.
Example: Consider this 2018 edition code:
let numbers = vec![1, 2, 3];
for num in numbers { // This would work by reference in 2018
println!("{}", num);
}
In Edition 2021, this might require an explicit reference if you don’t want to consume numbers:
let numbers = vec![1, 2, 3];
for num in &numbers { // Explicitly iterate by reference
println!("{}", num);
}
Or, if you intended to consume it:
let numbers = vec![1, 2, 3];
for num in numbers.into_iter() { // Explicit consume
println!("{}", num);
}
Diagnosis: If you see an error like "cannot infer type for _" or a type mismatch within a for loop after upgrading, this is likely the cause.
Fix: Add .into_iter(), &, or &mut to the collection in your for loop statement.
Why it Works: This explicitly tells Rust how you intend to iterate over the collection, satisfying the IntoIterator trait requirement for the exact type being iterated.
2. panic! macro changed
The Change: The panic! macro now requires a &'static str for its first argument. Previously, it accepted a String or other types that could be converted into a string.
Why it Matters: This encourages more static, compile-time known panic messages, which can improve performance and make it easier to reason about panic sites.
Example: This 2018 code:
let value = 5;
panic!("Value is too high: {}", value); // Works in 2018
Would now require a change:
let value = 5;
// You can't directly include a non-static string here anymore.
// You'd typically log the value and then panic with a static string.
// A common workaround is to use `eprintln` before panicking.
eprintln!("Value is too high: {}", value);
panic!("An expected error occurred.");
Diagnosis: Errors like "expected &'static str, found String" or similar type mismatches when calling panic!.
Fix: If you need to include dynamic information in a panic message, print it to stderr (e.g., using eprintln!) before calling panic! with a static string.
Why it Works: This separates the dynamic error reporting from the static panic message, adhering to the new panic! signature.
3. extern crate removed
The Change: The extern crate syntax is no longer necessary or allowed in Edition 2021. Rust automatically imports crates based on your Cargo.toml.
Why it Matters: This simplifies use declarations and makes the code cleaner.
Example: In Edition 2018, you might have had:
extern crate my_crate;
use my_crate::some_function;
In Edition 2021, this becomes:
use my_crate::some_function;
Diagnosis: A compiler error stating "crate my_crate is private" or "unused extern crate declaration" if you’re trying to use extern crate in 2021 edition.
Fix: Remove the extern crate my_crate; line.
Why it Works: The Rust compiler automatically makes crates listed in Cargo.toml available for use statements.
4. ? operator with Result types
The Change: The ? operator now requires the error type to implement From for the error type of the Result it’s propagating. This is a more precise version of the previous behavior.
Why it Matters: This improves type safety and makes error propagation more explicit. It ensures that when you use ?, the error can be seamlessly converted to the Result type of the enclosing function.
Example:
If you have a function returning Result<T, MyError> and you use ? on an operation returning Result<U, OtherError>, OtherError must implement From<OtherError> for MyError.
// In 2018 edition, some implicit conversions might have happened.
// In 2021, this is more strict.
fn process_data() -> Result<(), MyError> {
let data = read_file()?; // read_file returns Result<String, io::Error>
// If MyError doesn't implement From<io::Error>, this will fail.
Ok(())
}
// You'd need to add this implementation:
impl From<std::io::Error> for MyError {
fn from(err: std::io::Error) -> Self {
MyError::Io(err.to_string()) // Example conversion
}
}
Diagnosis: Errors like "the trait From<io::Error> is not implemented for MyError" or similar.
Fix: Implement the From trait for your error type, converting the incoming error type into your function’s error type.
Why it Works: This explicitly defines how to transform one error type into another, allowing the ? operator to correctly propagate errors.
5. async/await syntax
The Change: While async/await was stable in Edition 2018, Edition 2021 offers some refinements and stricter rules, particularly around futures and their integration with other language features. It’s less of a breaking change and more of a tightening of existing patterns.
Why it Matters: Ensures that asynchronous code behaves as expected and integrates smoothly with the rest of the language.
Diagnosis: Less common for direct breaking changes, but you might encounter issues if your async code relied on very implicit or underspecified behavior from the 2018 edition. Errors might be subtler, related to future polling or executor integration.
Fix: Ensure you are using a well-maintained async runtime (like tokio or async-std) and that your future implementations are robust. Often, upgrading the async runtime crate itself will resolve compatibility issues.
Why it Works: Modern async runtimes and the language evolution ensure that futures are handled efficiently and correctly by the executor.
The most common next error after fixing Edition 2021 migration issues is usually related to a dependency that hasn’t yet been updated to be compatible with the new edition, leading to compiler errors within the dependency’s code.