--- title: "Rust Basics - Module 3: Borrowing and References" description: "Learn how Rust's borrowing and references work - immutable vs mutable refs, borrow checker rules, dangling pointers, &str vs String, and slices explained." author: "Samba Siva" published: 2026-05-08T02:45:49.685Z updated: 2026-11-07T01:15:54.829Z tags: - "Technical" - "Rust" url: "https://sambasivareddy.in/blog/rust-basics-module-3-borrowing-and-references" --- # Rust Basics - Module 3: Borrowing and References In the previous blog post, we have discussed about the **"Ownership"** and its three rules of ownership and how it enforces only one owner for the given data at the given time, so that our code is free from memory bugs. In this post, we'll cover references, borrowing rules, the borrow checker in action, and slices. If ownership is the rule, borrowing is the exception that makes the rule practical. Without borrowing, every function call would consume its arguments, and that's genuinely painful to work with. * * * ## Part 1: The Core Idea - References **A Reference is a pointer to a value that you do not own**. We are borrowing it, like lending a book. The lender still owns it. When the borrow ends, the book goes back. The syntax is `&T` for an immutable reference, `&mut T` for a mutable one. ```rust let s = String::from("hello"); let r = &s // r borrows s - s still owns the heap data println!("{}", r); // Valid, prints "hello" println!("{}", s); // Valid, prints "hello" ``` **The** `&` **in** `&s` **creates a reference**. The value `r` on the stack is just a pointer to the address of `s` . It does not own the heap data. **_When_** `r` **_goes out of scope, nothing is freed_**. When `s` goes out of scope, the heap is freed - because `s` is still the owner. ```rust Stack ---------------------- r(&String) ptr --> s ---------------------- s(owner) ptr -> heap len: 5 cap: 5 -------------> Heap | Heap data 'h' 'e' 'l' 'l' 'o' | ---------------------- ``` * * * ## Part 2: Immutable vs Mutable References Rust has exactly two references types, and the rules around them are asymmetric by design. 1. Immutable Reference `&T` 2. Mutable Reference `&mut T` An immutable reference `&T` **lets you read the value**. **You can have as many of these as you want simultaneously**. No one can modify the value while any immutable reference exists. A mutable reference `&mut T` **lets you both read and write the value**. **You can have exactly one at a time**. No immutable references can exist simultaneously with a mutable one. ```rust let mut s = String::from("hello"); let r1 = &s; // fine, read-only let r2 = &s; // fine, read-only // multiple immutable refs allowed at a same time println!("{} {}", r1, r2); let r3 = &mut s; // fine, r1 & r2 are no longer used after above println r3.push_str(" world"); // r3 is a mutable one, so it can make changes to s's heap println!("{}", r3); // Not allowed, error: cannot borrow `s` as mutable because it is also borrowed as immutable let r4 = &s; let r5 = &mut s; println!("{} {}", r4, r5); ``` > The rule stated precisely: **at any given point in the code, you may have either one mutable reference OR any number of immutable references - never both at the same time.** * * * ## Part 3: The Borrow Checker in action ### Case 1: Simultaneous Mutable and Immutable References ```rust let mut s = String::from("hello"); let r1 = &s; // immutable borrow begins let r2 = &mut s; // cannot borrw s as mutable because r1 is still alive println!("{}", r1); ``` If this were allowed, `r2` could modify `s` 's heap buffer, while `r1` still holds the old pointer - classic use-after-free case. ### Case 2: Two mutable references ```rust let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; // cannot borrow s as mutable "more than once" r1.push_str(" world"); ``` If both `r1` and `r2` could mutate `s` simultaneously, we have a **data race** \- even on a single thread, the mutation order is ambiguous and the internal state (ptr, len, cap) in the stack could become inconsistent. ### Case 3: Dangling Reference ```rust fn dangle() -> &String { // returning a reference to local data let s = String::from("hello"); &s // 's' will be dropped when function returns } ``` The compiler rejects this: `s` will be freed/deallocated when `dangle` returns, so the reference would point to freed memory when we tried to access it. In C this compiles fine and produces a dangling pointer. Rust refuses to compile it. The solution is to return the `String` itself by giving up the ownership to the caller ```rust fn no_dangle() -> String { let s = String::from("hello"); s } ``` This works fine. Ownership is moved, and nothing is deallocated. * * * ## Part 4: &str VS String - a crucial distinction Now that we understand references, `&str` should make complete sense. `String` is an owned, heap-allocated, growable string. `&str` is a borrowed reference to a string slice, it is just a pointer and a length. It can point into a `String` 's heap buffer, or into a string literal in the binary's read-only segment. ```rust let s: String = String::from("hello world"); let slice: &str = &s[0..5]; // borrows 5 bytes of s's heap buffer let literal: &str = "hello"; // points into read-only binary segment ``` The idiomatic guideline: if your function only needs to read a string, take `&str` - not `&String` . This is more flexible because both `String` and `&str` can be passed as `&str` : ```rust fn print_it(s: &str) { println!("{}", s); } let owned = String::from("hello"); print_it(&owned); // &String auto-coerces to &str print_it("world"); // &str literal works directly ``` This auto-coercion is called **_de-ref coercion_** \- `&String` automatically becomes `&str` when the context demands it. We will discuss more about this in the traits module. Before we wrap up, there's one more borrowed type worth understanding - **slices**. * * * ## Part 5: Slices - references to a contiguous sequence The same idea generalises beyond strings. A slice `&[T]` is a reference to a contiguous portion of an array or `Vec`. ```rust let arr = [1, 2, 3, 4, 5]; let slice: &[i32] = &arr[1..4]; // borrows elements 2,3,4 println!("{:?}", slice); // [2,3,4] ``` A slice is always a fat pointer - two words on the stack: a pointer to the first element, and the length. **No ownership, no allocation.** * * * ## Conclusion Borrowing is Rust's answer to a hard problem: how do you share data without copying it, and without the chaos of unchecked aliasing? The rules are simple to state but deep in consequence: 1. References let you read or modify data without owning it 2. Immutable references can coexist freely; mutable references demand exclusivity 3. The borrow checker enforces these rules at compile time — no runtime cost, no garbage collector needed Once these rules click, &str, slices, and the broader Rust type system start to feel less like restrictions and more like a contract that makes your code provably safe. Ownership tells you who is responsible. Borrowing tells you who is allowed to look or touch and for how long. In the next post, we'll move into Structs, Enums and Pattern Matching.