In 2024, the Stack Overflow Developer Survey voted Rust the most admired language. Rust's unique combination of performance, safety, and ergonomics has made it a popular choice for systems programming, web development, blockchain, and more.
However, as with any powerful tool, getting the most out of Rust requires a solid understanding of its fundamentals.
In this article, I want to share three common mistakes beginners often make when learning Rust and practical solutions to help you avoid them.
All the code is available and open source here: github.com/FrancescoXX/three-rust-mistakes
Let's get started!
1. Confusion Around the let Keyword
If you're from a JavaScript background, you might find Rust's let
keyword familiar yet surprisingly different.
Example:
In JavaScript, you can reassign a variable declared with let
:
let x = 10;
x = 20; // Reassignment is allowed
console.log(x); // Outputs: 20
..But if we try to do the same in Rust, we'll encounter a compile-time error:
fn main() {
let x = 10;
x = 20; // This will cause a compile-time error in Rust
println!("{}", x);
}
Why? In Rust, variables are immutable by default, meaning you can't reassign a value unless you explicitly declare the variable as mutable:
While let
in both languages is used to declare variables, Rust’s strict handling of mutability and scope can be a source of confusion.
Solution:
To allow reassignment, you must declare the variable as mutable using the mut
keyword:
fn main() {
let mut x = 10; // Declare x as mutable
x = 20; // Now reassignment is allowed
println!("{}", x); // Outputs: 20
}
If you want to learn more about variables in Rust, check out this YouTube video
2. Misunderstanding Ownership and Borrowing
One of Rust's most distinctive and powerful features is its ownership model.
While this model ensures memory safety without a garbage collector, it can be tricky for beginners to grasp fully.
Misunderstanding ownership and borrowing often leads to common errors like "value borrowed after move" or "cannot borrow as mutable."
Example:
Consider the following code:
fn main() {
let s = String::from("Hello, Rust!");
let s2 = s; // Ownership moves to s2, s is no longer valid
println!("{}", s); // This will cause a compile-time error
}
In this example, when we assign s to s2, the ownership of the data is transferred to s2. As a result, s is no longer valid, and attempting to use it will cause a compile-time error!
Solution:
To avoid this mistake, you can borrow
the value instead of transferring ownership.
Borrowing allows you to reference the original value without taking ownership, ensuring that the original variable remains valid:
fn main() {
let s = String::from("Hello, Rust!");
let s2 = &s; // Borrowing s, s is still valid
println!("{}", s); // This will work
println!("{}", s2); // This will also work
}
By using a &s
reference , we can access the value without transferring ownership, allowing both s and s2 to coexist without errors.
Understanding ownership and borrowing is crucial to mastering Rust.
If you want a deeper dive into these concepts, check out this YouTube video on Rust's ownership model.
3. Ignoring Error Handling
While it's tempting to ignore error checks and use unwrap
to quickly access values, doing so can lead to unexpected panics and runtime errors if something goes wrong.
Rust's Result
type provides a powerful mechanism for handling errors safely and concisely, ensuring your program can gracefully deal with unexpected situations.
Example:
Consider the following code snippet that reads a file and prints its contents:
use std::fs::File;
use std::io::Read;
fn main() {
let file = File::open("example.txt");
let mut contents = String::new();
file.unwrap().read_to_string(&mut contents).unwrap(); // This can panic if the file doesn't exist
println!("{}", contents);
}
In this example, the open
method returns a Result
type that represents the operation's success or failure.
Using unwrap
on the Result
can cause the program to panic if the file doesn’t exist or if there's another error.
Solution:
To handle errors properly, you can use the match expression to check the Result and handle both success and failure cases:
use std::fs::File;
use std::io::{self, Read};
fn main() {
let file = File::open("example.txt");
match file {
Ok(mut f) => {
let mut contents = String::new();
match f.read_to_string(&mut contents) {
Ok(_) => println!("{}", contents),
Err(e) => eprintln!("Error reading file: {}", e),
}
}
Err(e) => eprintln!("Error opening file: {}", e),
}
}
In this solution, instead of using unwrap
, we handle the Result explicitly with a match
statement (here is a video about the match statement).
This approach ensures errors are caught and handled, preventing unexpected panics and making the code more robust.
Using the ? Operator:
For even more concise error handling, Rust provides the ?
operator, which can be used to propagate errors up the call stack:
use std::fs::File;
use std::io::{self, Read};
fn read_file() -> io::Result<String> {
let mut file = File::open("example.txt")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() {
match read_file() {
Ok(contents) => println!("{}", contents),
Err(e) => eprintln!("Error: {}", e),
}
}
Here, the ?
operator handles errors in a way that keeps the code clean and readable while still ensuring that errors are correctly managed.
If you want an introduction to Error Handling in Rust, check out this YouTube video.
Conclusion
Rust is an excellent language, but it takes time to get used to its quirks and unique features, like any new tool.
Remember, everyone makes mistakes, and it's normal while learning a new programming language, especially one as powerful and unique as Rust.
If you want to learn more about Rust, I have a Rust YouTube playlist where I post a complete course for beginner Rust developers.
All the code is available and open source here: github.com/FrancescoXX/three-rust-mistakes
Have fun writing Rust code!
Francesco