Intro
I've been learning and writing Rust for about 7 years now, but I still remember the beginner frustration where everything felt really difficult and I was constantly fighting the compiler even to do basic things. Getting started with Rust can be challenging but teaching a lot of beginners over the last few years, I started to see some common pitfalls or patterns that you can start using today to improve your learning curve.
Tip 1: Use unwrap for Options and Results liberally at first
Options and Results are new for someone coming from a language without them. Focus on making stuff work first by unwrapping to make your code work. Then learn about how to deal with them idiomatically later. Also you're most likely writing some throw-away code initially, trying to make something work so error handling won't be your first priority, so I'd recommend unwrapping liberally, then doing a second pass to improve error handling, something like this.
impl Database {
pub fn find_student(&self, name: &str) -> Option<&Student> {
/// ...
}
}
pub fn main() {
// Load database with all students
let database = Database::new();
// Find a specific student and print their id
let student_id = database.find_student("Bob").unwrap().id;
println!("Bob's id is {}", student_id);
}
Then in the second iteration this code becomes something like this.
Once your code is working and you understand the error cases better, transition away from unwrap() in production code where unexpected panics could crash your application - that's when proper error handling with match, if let, or the ? operator becomes important for robustness. Here we use a match.
pub fn main() {
// Load database with all students
let database = Database::new();
// Find a specific student and print their id
match database.find_student("Bob") {
Some(bob) => println!("Bob's id is {}", bob.id);
None => println!("Didn't find Bob");
}
}
Tip 2: Learn the difference between String and &str
This is fundamental to understand, and don't be afraid to use String only at first even if it's terribly inefficient. You can optimize later once you understand the concepts better.
Understanding slices will also help here - slices are essentially borrowed views into contiguous sequences of data. Think of &str
as a string slice that references existing string data without owning it. For example, &s[0..5]
creates a slice that references the first 5 characters of string s
without copying them. This concept applies to both strings and arrays/vectors (where &[T]
is an array slice). Start by using them to reference parts of existing data, and you'll gradually understand how they help with memory efficiency.
fn main() {
// String - owned, heap-allocated, mutable
let mut owned_string = String::from("Hello");
owned_string.push_str(" World"); // Can modify
// &str - borrowed reference to string data, immutable
let string_slice: &str = "Hello World"; // String literal
let borrowed_slice: &str = &owned_string; // Borrowing from String
// Function that takes &str can accept both
print_message(&owned_string); // String automatically converts to &str
print_message(string_slice); // &str passed directly
}
fn print_message(msg: &str) {
println!("{}", msg);
}
Tip 3: Clone liberally (but do not Copy)
At first you'll get the compiler shouting at you a lot because you're moving stuff without knowing you're moving it. Add a Clone derive annotation, call .clone()
and move on to the next problem! Then as a second pass come back and optimize.
#[derive(Clone)]
struct Student {
name: String,
id: u32,
}
fn process_student(student: Student) {
println!("Processing student: {}", student.name);
}
fn main() {
let alice = Student {
name: "Alice".to_string(),
id: 12345,
};
process_student(alice.clone()); // Clone so we don't move alice
println!("Alice is still available: {}", alice.name); // This works!
}
Tip 4: Avoid Storing References in Structs
As soon as you do this you'll need to get into lifetimes and if you're still learning the basics, I'd avoid this. Just store owned types, and pass everything else as arguments to functions.
// AVOID
struct Course<'a> {
student: &'a Student, // Borrowed, needs explicit lifetimes
}
// GOOD
struct Course {
student: Student, // Owns the data
}
Tip 5: Avoid linked lists and trees
As soon as you get into this you need to understand borrowing and Box/Rc/Arc which you will struggle with as a beginner. Once you feel comfortable with the basics, then dive deeper into these advanced data structures. There is even a whole book about writing many flavours of linked lists: Learn Rust With Entirely Too Many Linked Lists which I'd recommend reading only after you learn at least some of the core rust pointer types.
Tip 6: Consider Ordering and Use Scope Blocks
This is my favourite trick, if you plan to do multiple mutations, or a read, write then read again, ordering plus creating a new scope block for each can save the day as it releases the borrow.
struct Student {
name: String,
grade: u8,
}
impl Student {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
grade: 100,
}
}
pub fn grade_mut(&mut self) -> &mut u8 {
&mut self.grade
}
pub fn name(&self) -> &str {
&self.name
}
}
fn main() {
let mut student = Student::new("Alice");
{
let grade = student.grade_mut();
*grade += 1;
} // grade's mutable borrow is dropped here
let name = student.name(); // Now this works
println!("Name: {}", name);
}
/// WORKS!!
Compare this to the problematic version:
fn main() {
let mut student = Student::new("Alice");
let grade = student.grade_mut(); // First mutable borrow
let name = student.name(); // Try to borrow immutably immediately
*grade += 1; // Still using mutable borrow
println!("Name: {}, Grade: {}", name, grade);
}
// Error: cannot borrow `student` as immutable because it is also borrowed as mutable
Tip 7: Use Option::take() to Move Values Safely
If you need to move a value out of a struct, Option<T>::take()
prevents borrowing conflicts:
struct Database {
student: Option<Student>,
}
impl Database {
fn remove_student(&mut self) -> Option<Student> {
self.student.take() // ✅ Replaces with None, avoiding borrowing issues
}
}
let mut db = Database { student: Some(Student::new("Charlie")) };
let removed_student = db.remove_student(); // ✅ Student is safely moved out
Tip 8: Pass Data as Function Arguments Instead of Storing It
The extra step of storing owned values is not storing values at all. Instead of holding a reference/owned value inside a struct, pass data when needed, and when it makes sense. It's also easier to pass references around without dealing with lifetimes.
struct Course;
impl Course {
fn process(student: &mut Student) {
println!("{} is processed!", student.name);
}
}
let alice = Student::new("Alice");
Course::process(&mut alice); // ✅ Passes a reference instead of storing it
Conclusion
These practical tips worked for me initially, and as I got more comfortable with the language, I started going deeper into each of these topics and learning how to do something in the most idiomatic and efficient way.
Hope it helped you learn 1% more Rust today, I write a newsletter like this every Thursday subscribe to receive this, and follow me on Bluesky 🦋 🦀
Top comments (1)
Maybe in the future it would be helpful! Saving right now