Rust Week 2: Diving Deep into Ownership, Collections, and Error Handling

My journey through the core concepts that make Rust unique and powerful
Introduction
Welcome back to my Rust learning journey. After getting comfortable with the basics in Week 1, Week 2 was where things got really interesting. This week, I dove headfirst into what makes Rust truly special - its ownership model, borrowing system, and the powerful ways it handles data without a garbage collector.
If Week 1 was learning to walk, Week 2 was learning to run while juggling. Let me share everything I learned.
1. Ownership - Rust's Superpower
What is Ownership?
Ownership is Rust's most unique feature. It enables Rust to make memory safety guarantees without needing a garbage collector. In simple terms, ownership is a set of rules that the compiler checks at compile time.
Think of ownership like being the owner of a house. When you own a house, you are responsible for it. When you move out, you clean up after yourself. Rust works the same way with memory - each value has a single owner, and when that owner goes out of scope, Rust automatically cleans up the memory.
fn main() {
let house = String::from("My House");
}
Stack vs Heap
Understanding stack and heap is crucial for grasping ownership.
The stack stores values in the order it gets them and removes them in the opposite order. This is called last in, first out. All data stored on the stack must have a known, fixed size.
The heap is less organized. When you put data on the heap, you request a certain amount of space. The operating system finds an empty spot somewhere on the heap, marks it as being in use, and returns a pointer to that location.
Imagine the stack as a tidy desk where you keep your notebook, pen, and coffee mug. Everything has its place and size, and you can grab things quickly. The heap is like a warehouse where you store boxes of various sizes - you need to find space, put things away, and remember where you put them.
let x = 5;
let s = String::from("hello");
Move vs Copy
Some types in Rust are like original documents - they cannot be copied, only moved. Others are like photocopies - they can be duplicated freely.
When you assign an integer to another variable, a copy is created. Both variables can be used independently.
When you assign a String to another variable, the ownership moves. The original variable can no longer be used because the ownership of the heap memory has been transferred.
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s2);
Types like integers, booleans, and floating-point numbers implement the Copy trait. Types like String, vectors, and other heap-allocated types do not.
2. Borrowing and References
What is Borrowing?
When a function wants to use a value but not take ownership, it can borrow the value using references. Borrowing allows you to access data without taking ownership.
Think of it like borrowing a book from the library. You can read it, but you don't own it, and you must return it.
Immutable References
An immutable reference allows you to read data but not modify it. You can have multiple immutable references to the same data at the same time.
fn main() {
let book = String::from("Rust Programming");
let reader1 = &book;
let reader2 = &book;
println!("Readers see: {} and {}", reader1, reader2);
}
Mutable References
A mutable reference allows you to both read and modify data. However, you can only have one mutable reference to a particular piece of data at a time.
fn main() {
let mut book = String::from("Rust Programming");
let editor = &mut book;
editor.push_str(" - 2nd Edition");
println!("Edited book: {}", book);
}
The Rules of Borrowing
You can have either one mutable reference or any number of immutable references, but not both at the same time.
References must always be valid - no dangling references.
These rules prevent data races at compile time. A data race happens when:
Two or more pointers access the same data at the same time
At least one of them is writing to the data
There's no mechanism to synchronize access
3. Slices
What are Slices?
Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection. A slice does not take ownership - it's a view into a portion of data.
Think of slices like looking through a window at a specific part of a landscape. You don't own the land, but you can see a portion of it.
fn main() {
let text = String::from("Hello, Rust!");
let hello = &text[0..5];
let rust = &text[7..11];
println!("First word: {}", hello);
println!("Second word: {}", rust);
}
String Slices
A string slice is a reference to part of a String. The syntax &text[0..5] creates a slice starting at index 0 and continuing up to, but not including, index 5.
String slices have the type &str.
Array Slices
Slices work with arrays too. They give you a view into a portion of an array.
let numbers = [1, 2, 3, 4, 5];
let slice = &numbers[1..4];
String vs &str
Stringis an owned type that stores its data on the heap. You can grow, shrink, and modify it.&stris a borrowed reference to a string. It's a view into a string that you don't own.
When you need to own string data, use String. When you just need to read string data, use &str.
4. Structs
What are Structs?
A struct is a custom data type that lets you name and package together multiple related values. Think of it as creating a blueprint for a concept in your program.
struct User {
username: String,
email: String,
active: bool,
age: u32,
}
Creating Instances
To use a struct, you create an instance by providing concrete values for each field.
fn main() {
let user1 = User {
username: String::from("rustacean"),
email: String::from("rust@example.com"),
active: true,
age: 25,
};
}
Accessing and Modifying Fields
You access struct fields using dot notation. To modify a field, the entire instance must be mutable.
fn main() {
let mut user1 = User {
username: String::from("rustacean"),
email: String::from("rust@example.com"),
active: true,
age: 25,
};
println!("User: {}", user1.username);
user1.email = String::from("newemail@example.com");
}
Field Init Shorthand
When variable names match field names, you can use a shorthand syntax.
fn build_user(username: String, email: String) -> User {
User {
username,
email,
active: true,
age: 30,
}
}
5. impl and Methods
What are Methods?
Methods are functions attached to structs. They define the behavior of your structs. The first parameter of a method is always self, which represents the instance the method is being called on.
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn double_width(&mut self) {
self.width *= 2;
}
}
Types of Method Parameters
&self- borrows the instance immutably, allowing read-only access&mut self- borrows the instance mutably, allowing modificationself- takes ownership of the instance, rarely used
Associated Functions
Functions defined in an impl block that don't take self as a parameter are called associated functions. They're often used as constructors.
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let square = Rectangle::square(15);
}
Associated functions are called using double colons: Rectangle::square(15).
6. Enums
What are Enums?
Enums allow you to define a type by enumerating its possible variants. They're perfect for situations where a value can be one of several possibilities.
Think of it like ordering coffee - it can be espresso, latte, or cappuccino, each with different properties.
enum Coffee {
Espresso,
Latte { milk_type: String, shots: u8 },
Cappuccino(u8),
}
Enum Variants
Enums can have different kinds of variants:
Unit variants - just the name, no associated data
Struct variants - named fields like a struct
Tuple variants - unnamed fields like a tuple
Pattern Matching with match
The match expression allows you to compare a value against a series of patterns and execute code based on which pattern matches.
fn describe_coffee(coffee: Coffee) {
match coffee {
Coffee::Espresso => println!("Strong and short"),
Coffee::Latte { milk_type, shots } => {
println!("Latte with {} milk, {} shots", milk_type, shots);
}
Coffee::Cappuccino(shots) => {
println!("Cappuccino with {} shots", shots);
}
}
}
The Power of Enums
Enums in Rust are extremely powerful because they can hold data. This is different from enums in many other languages where they're just numbered constants. This feature makes error handling with Option and Result possible.
7. Vectors
What are Vectors?
Vectors (Vec<T>) are resizable arrays. They store multiple values of the same type in a single data structure, with all values placed next to each other in memory.
Think of them as a backpack where you can keep adding and removing items.
fn main() {
let mut v1: Vec<i32> = Vec::new();
let mut v2 = vec![1, 2, 3];
}
Creating Vectors
You can create vectors in two ways:
Using
Vec::new()- creates an empty vectorUsing the
vec!macro - creates a vector with initial values
Adding and Removing Elements
pushadds an element to the endpopremoves the last element and returns it asOption<T>
v1.push(10);
v1.push(20);
let last = v1.pop();
Accessing Elements
You can access vector elements by index using either &v[index] or the get method.
let third = &v2[2];
match v1.get(1) {
Some(value) => println!("Value: {}", value),
None => println!("No value"),
}
The indexing syntax panics if the index is out of bounds. The get method returns an Option that you can handle safely.
Iterating Over Vectors
There are three ways to iterate over vectors, each with different ownership implications:
let v = vec![1, 2, 3];
for i in &v {
println!("{}", i);
}
let mut v_mut = vec![1, 2, 3];
for i in &mut v_mut {
*i *= 2;
}
for i in v {
println!("{}", i);
}
for i in &v- borrows immutably, v remains usablefor i in &mut v- borrows mutably, v remains usablefor i in v- takes ownership, v is consumed
8. HashMap
What is HashMap?
A HashMap<K, V> stores a mapping of keys of type K to values of type V. It does this using a hashing function that determines how to place these keys and values in memory.
Think of it like a dictionary - you look up words to find their definitions.
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
}
Accessing Values
You can get values from a HashMap using the get method, which returns an Option<&V>.
let team_name = String::from("Blue");
match scores.get(&team_name) {
Some(score) => println!("Score: {}", score),
None => println!("Team not found"),
}
Iterating
You can iterate over key-value pairs in a HashMap.
for (key, value) in &scores {
println!("{}: {}", key, value);
}
Updating a HashMap
When you want to insert a value only if the key has no value, you can use the entry API.
scores.entry(String::from("Blue")).or_insert(25);
scores.entry(String::from("Red")).or_insert(30);
The or_insert method returns a mutable reference to the value for this key. If the key exists, it returns a reference to the existing value. If not, it inserts the parameter as the new value and returns a reference to that.
Updating Based on Old Value
A common pattern is to look up a key's value and then update it based on the old value.
let text = "hello world wonderful world";
let mut word_counts = HashMap::new();
for word in text.split_whitespace() {
let count = word_counts.entry(word).or_insert(0);
*count += 1;
}
This counts the occurrences of each word in the text.
9. Option
What is Option?
Option<T> is an enum that represents the concept of a value being present or absent. It's Rust's way of handling null values without the problems of null references.
enum Option<T> {
Some(T),
None,
}
Think of it like a box that either contains something or is empty.
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
if denominator == 0.0 {
None
} else {
Some(numerator / denominator)
}
}
Why Option Instead of Null?
In languages with null, every variable can potentially be null, leading to null pointer exceptions. In Rust, you must explicitly handle the possibility of a missing value by using Option<T>. This makes your code safer because you can't forget to handle the None case.
Using Option with match
The primary way to work with Option is using match to handle both cases.
fn main() {
let result1 = divide(10.0, 2.0);
match result1 {
Some(value) => println!("Result: {}", value),
None => println!("Cannot divide by zero"),
}
}
if let Syntax
When you only care about the Some case, you can use if let for a more concise syntax.
if let Some(value) = result1 {
println!("Value: {}", value);
}
Common Option Methods
Option<T> has many useful methods:
unwrap()- returns the value if Some, panics if None (use cautiously)expect("message")- similar to unwrap but with a custom panic messageunwrap_or(default)- returns the value if Some, otherwise returns defaultmap()- applies a function to the contained value if Some
10. Result<T, E>
What is Result?
Result<T, E> is an enum for operations that can succeed or fail. It's similar to Option but includes error information when something goes wrong.
enum Result<T, E> {
Ok(T),
Err(E),
}
Think of it as a receipt from a vending machine - either you get your snack or an error message.
use std::fs::File;
use std::io::Read;
fn read_username_from_file() -> Result<String, std::io::Error> {
let mut file = match File::open("hello.txt") {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
Handling Results with match
You use match with Result just like with Option.
fn main() {
match read_username_from_file() {
Ok(name) => println!("Username: {}", name),
Err(error) => println!("Error reading file: {}", error),
}
}
The ? Operator
The ? operator is a shorthand for the common pattern of returning early on error. It does almost exactly what the match statement does.
fn read_username_concise() -> Result<String, std::io::Error> {
let mut file = File::open("hello.txt")?;
let mut username = String::new();
file.read_to_string(&mut username)?;
Ok(username)
}
If the value is Ok, the ? operator returns the value inside. If the value is Err, it returns the error from the function early.
When to Use Result
Use Result when an operation can fail in ways you need to communicate to the caller. File I/O, network requests, and data parsing are common examples.
Key Takeaways from Week 2
Ownership is liberating, not restricting - Once it clicks, you realize it prevents entire classes of bugs. The compiler ensures memory safety without a garbage collector.
Borrowing rules feel strict but prevent data races - No more wondering who modified what. The rules guarantee that data races cannot happen at compile time.
Enums with match make code expressive - Pattern matching forces you to handle all cases, making your code more robust and self-documenting.
Option and Result force you to handle edge cases - No more null pointer exceptions. The type system ensures you handle both success and failure cases.
Vectors and HashMaps require ownership awareness - Always think about whether you are borrowing or moving. Understanding this prevents subtle bugs.
Structs and impl blocks organize code - They help you group related data and behavior together, making your code more maintainable.
Slices provide safe views into data - They let you work with parts of collections without copying or taking ownership.
What's Next
Week 3 will dive into more advanced topics:
Error handling mastery with the question mark operator and custom error types
Generics for writing flexible, reusable code
Traits for defining shared behavior
Lifetimes for ensuring references are always valid
Writing tests to verify your code works correctly
Final Thoughts
Rust's learning curve is steep, but Week 2 showed me why it is worth it. The ownership model, which seemed so restrictive at first, actually gives me confidence that my code will work as intended. No more unexpected nulls, no more data races, no more memory leaks - just clean, predictable behavior.
The type system in Rust doesn't just catch errors - it guides you toward better design. When you struggle with the borrow checker, it's often because your code has a fundamental design issue that would have caused problems in other languages anyway.
The best analogy I have found for Rust is that it is like having a strict but helpful mentor who catches your mistakes before they become problems. Yes, you have to follow the rules, but those rules make you a better programmer.
As I continue this journey, I'm excited to see what Week 3 brings. The foundation from Week 2 is solid, and I feel ready to tackle more advanced concepts.
Happy coding, and remember: the Rust compiler is your friend, not your enemy. It's there to help you write better, safer code.
Connect with :
Hashnode: hashnode.com/@Nehal71
Twitter : twitter.com/IngoleNehal
LinkedIn: linkedin.com/in/nehal-ingole
GitHub : github.com/Ingole712521





