Reading old Rust
Code written before 2018 still compiles, and it looks different: try! where the question mark belongs, a trait object without dyn, a use path that starts at the crate root from inside a module. This lesson reads three of those and rewrites one module.
Each program is compiled twice, once with the edition it was written for and once with 2024. The old build runs and the new one reports what changed, which is the pair of outputs a migration starts from.
The try! macro
try! was the question mark before the operator existed: it returned the error from the function or unwrapped the value. The macro is deprecated in every edition and an error from 2018 on, because try became a reserved word.
The program parses a string with the macro. The 2015 build prints a warning and runs, and the 2024 build stops at the call.
fn parse(text: &str) -> Result<i32, std::num::ParseIntError> {
let value = try!(text.parse());
Ok(value)
}
fn main() {
println!("{:?}", parse("42"));
}
rustc --edition 2015 try_macro.rs
./try_macro
rustc --edition 2024 try_macro.rs
warning: use of deprecated macro `try`: use the `?` operator instead
--> try_macro.rs:2:17
|
2 | let value = try!(text.parse());
| ^^^
|
= note: `#[warn(deprecated)]` on by default
Ok(42)
error: use of deprecated `try` macro
--> try_macro.rs:2:17
|
2 | let value = try!(text.parse());
| ^^^^^^^^^^^^^^^^^^
|
= note: in the 2018 edition `try` is a reserved keyword, and the `try!()` macro is deprecated
help: you can use the `?` operator instead
|
2 - let value = try!(text.parse());
2 + let value = text.parse()?;
| +
The replacement is a suffix instead of a wrapper, so the line becomes shorter and the Ok(value) below it can disappear with it. The lesson on Result is the modern form.
A bare trait object
A trait used as a type was accepted until 2018 and deprecated then, because Box<Error> reads like a box of a trait, which has no size. dyn Error says that the box holds a trait object, and the keyword became mandatory in 2021.
The program takes a boxed error by value. The 2015 build warns and prints, and the 2021 build reports E0782 with the keyword to add.
use std::error::Error;
fn describe(error: Box<Error>) -> String {
error.to_string()
}
fn main() {
println!("{}", describe(Box::new(std::fmt::Error)));
}
rustc --edition 2015 bare.rs
./bare
rustc --edition 2021 bare.rs
warning: trait objects without an explicit `dyn` are deprecated
--> bare.rs:3:24
|
3 | fn describe(error: Box<Error>) -> String {
| ^^^^^
|
= warning: this is accepted in the current edition (Rust 2015) but is a hard error in Rust 2021!
= note: for more information, see <https://doc.rust-lang.org/edition-guide/rust-2021/warnings-promoted-to-error.html>
= note: `#[warn(bare_trait_objects)]` (part of `#[warn(rust_2021_compatibility)]`) on by default
help: if this is a dyn-compatible trait, use `dyn`
|
3 | fn describe(error: Box<dyn Error>) -> String {
| +++
an error occurred when formatting an argument
error[E0782]: expected a type, found a trait
--> bare.rs:3:24
|
3 | fn describe(error: Box<Error>) -> String {
| ^^^^^
|
help: you can add the `dyn` keyword if you want a trait object
|
3 | fn describe(error: Box<dyn Error>) -> String {
| +++
error: aborting due to 1 previous error
The line an error occurred when formatting an argument is the output of the 2015 program: the error value is a std::fmt::Error, whose message says exactly that. The lesson on trait objects explains why the keyword is needed.
Paths inside a module
In 2015 a use inside a module started at the crate root. Since 2018 the same line starts at the current module, so a name from the root is written crate::helper or reached with super::. The compiler reports E0432 and prints the corrected line.
The program has a helper module at the root and an inner module that imports it the old way. The 2015 build prints forty-two, and the 2021 build stops in the inner module.
mod helper {
pub fn double(value: i32) -> i32 {
value * 2
}
}
mod inner {
use helper::double;
pub fn run() {
println!("{}", double(21));
}
}
fn main() {
inner::run();
}
rustc --edition 2015 paths2.rs
./paths2
rustc --edition 2021 paths2.rs
42
error[E0432]: unresolved import `helper`
--> paths2.rs:8:9
|
8 | use helper::double;
| ^^^^^^
|
= note: `use` statements changed in Rust 2018; read more at <https://doc.rust-lang.org/edition-guide/rust-2018/module-system/path-clarity.html>
help: a similar path exists
|
8 | use crate::helper::double;
| +++++++
error: aborting due to 1 previous error
The note links to the page that explains the change, and the help shows the five characters that fix the line. A code base from 2015 usually has a few of these, and each one is a mechanical change.
The same code modernised
The three changes together turn the old module into code that builds with edition 2024: the import gains crate::, the box gains dyn, and the macro becomes the question mark. The program below is the import fixed, which is the change the compiler could not do for the reader.
The module compiles under both editions, and the 2024 build prints the same number as the 2015 one.
mod helper {
pub fn double(value: i32) -> i32 {
value * 2
}
}
mod inner {
use crate::helper::double;
pub fn run() {
println!("{}", double(21));
}
}
fn main() {
inner::run();
}
rustc --edition 2024 modern.rs
./modern
42
The order to work in is the one this lesson used: build the crate with the new edition, read the first error, fix it, and repeat. cargo fix --edition does the mechanical part in one run, and the previous lesson shows the command and what it leaves for the reader.
Exercises
- Replace try! with the question mark in the first program and build it under 2024.
- Add dyn to the second program and explain why the box still has a fixed size.
- Write a module that imports a sibling module with super:: instead of crate:: and check both editions.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What replaced the
try!macro?Answer
The question mark operator
The operator does the same early return as a suffix instead of a wrapper.
-
What does
dynmark inBox<dyn Error>?Answer
A trait object, whose concrete type is erased
The keyword says the box holds a value of some type that implements the trait.
-
What changed about
usepaths in 2018?Answer
They start at the current module, so crate root needs
crate::Path clarity made the root explicit, and the old line gained five characters.
-
Which error reports a bare trait object in edition 2021?
Answer
E0782, expected a type, found a trait
The trait in type position is the error the dyn keyword resolves.
-
What does this program print?
fn parse(text: &str) -> Result<i32, std::num::ParseIntError> { let value: i32 = text.parse()?; Ok(value * 2) } fn main() { println!("{:?}", parse("21")); }Answer
Ok(42)
The question mark inside a function that returns Result is the modern form, and the value doubles.
-
What does this program print?
mod helper { pub fn double(value: i32) -> i32 { value * 2 } } fn main() { println!("{}", helper::double(3)); }Answer
6
An absolute path from the crate root works in every edition.
-
What does this program print?
fn main() { let values = vec![1, 2, 3]; let text: String = values.iter().map(|value| value.to_string()).collect(); println!("{text}"); }Answer
123
A chain of strings is collected without a separator.
-
What does this program print?
fn main() { let boxed: Box<dyn std::fmt::Display> = Box::new(7); println!("{boxed}"); }Answer
7
The box formats through the trait object and prints the number.
-
What is the first line of the compiler output for this program when it is compiled with edition 2021?
use std::error::Error; fn describe(error: Box<Error>) -> String { error.to_string() } fn main() { println!("{}", describe(Box::new(std::fmt::Error))); }Answer
error[E0782]: expected a type, found a trait
Since 2021 a trait in type position is a hard error, and the help adds dyn.
-
What is the first line of the compiler output for this program when it is compiled with edition 2021?
mod helper { pub fn double(value: i32) -> i32 { value * 2 } } mod inner { use helper::double; pub fn run() { println!("{}", double(21)); } } fn main() { inner::run(); }Answer
error[E0432]: unresolved import
helperThe use path starts at the inner module, and the help suggests crate::helper.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev