Rust Borrowing and Lifetimes: Lending Values Without Dangling References

Author

Andres Monge

Published

May 13, 2026

Rust borrowing is easier to understand if you think about it as lending.

The owner has the value. A borrower can temporarily use it. But the borrower cannot keep using it after the owner is gone.

That is the core idea behind lifetimes.

let name = String::from("Andres");
let borrowed_name = &name;

println!("{borrowed_name}");

Here, name owns the String. borrowed_name only borrows it.

The borrowed reference is valid because the owned value is still alive.

Borrowing as Lending

When you pass &T, you are not giving the value away. You are lending read-only access.

fn print_name(name: &str) {
    println!("Name: {name}");
}

let name = String::from("Andres");

print_name(&name);

println!("Still mine: {name}");

The function borrows name, prints it, and then the borrow ends.

This is like lending someone a book for a moment. They can read it, but they do not own it.

If the borrower only needs to read, use &T.

If the borrower needs to change the value, use &mut T.

But in both cases, the borrowed access must not outlive the owned value.

Three Ways a Function Can Receive Data

When you design a function parameter, there are usually three choices.

1. Take ownership

fn eat_name(name: String) {
    println!("{name}");
}

Use this when the function should consume the value.

After the call, the caller no longer owns that String.

2. Borrow immutably

fn print_name(name: &str) {
    println!("{name}");
}

Use this when the function only needs to read.

The caller keeps ownership.

Notice that &str is often better than &String for read-only string input.

It accepts both string slices and borrowed String values, so it is more flexible.

3. Borrow mutably

fn add_excitement(name: &mut String) {
    name.push('!');
}

Use this when the function should modify the caller’s value without taking ownership.

So the short checklist is:

  • use T when the function should consume the value
  • use &T when the function should only read
  • use &mut T when the function should modify without owning

mut x: T Is Not the Same as x: &mut T

These are easy to confuse, but they mean different things.

fn own_and_change(mut numbers: Vec<i32>) {
    numbers.push(4);
}

Here the function owns numbers.

mut only means the local binding may change the owned value.

Now compare that with:

fn borrow_and_change(numbers: &mut Vec<i32>) {
    numbers.push(4);
}

Here the function does not own the vector.

It temporarily borrows the caller’s vector and modifies it through that borrow.

So in a parameter list:

  • mut before the name changes the local binding
  • & or &mut after the colon changes the parameter type

That distinction explains many beginner borrowing errors.

The Call Site Must Match the Kind of Borrow

If a function expects a mutable borrow, the call site must pass a mutable borrow.

fn add_excitement(name: &mut String) {
    name.push('!');
}

let mut name = String::from("Andres");
add_excitement(&mut name);

This works because:

  • the function expects &mut String
  • the caller passes &mut name
  • name is declared with mut

But this would fail:

let name = String::from("Andres");
add_excitement(&mut name);

The variable itself must be a mutable binding before it can be mutably borrowed.

The Dangling Reference Problem

A dangling reference is a reference to something that no longer exists.

Rust prevents this.

For example, this idea is not allowed:

fn bad_reference() -> &String {
    let message = String::from("hello");
    &message
}

The problem is not the &message part by itself. The problem is that message is created inside the function.

When the function ends, message is destroyed.

So returning &message would mean returning a reference to data that no longer exists.

In plain language:

You cannot lend something after it has already been thrown away.

The fix is to return owned data instead:

fn good_value() -> String {
    let message = String::from("hello");
    message
}

Now the function returns the actual String, not a borrowed reference to a dead local variable.

Lifetimes Are Not Magic

You will often see syntax like this:

&'a str

or this:

fn display_name<'a>(username: &'a str, fallback: &'a str) -> &'a str

The 'a part is a lifetime annotation.

But the most important thing is what it does not do.

'a does not:

  • keep data alive
  • allocate memory
  • copy a value
  • make a reference valid forever
  • extend a borrow by force

Instead, 'a names a relationship between references.

It tells Rust:

These borrowed values are connected. The returned reference is only valid while the borrowed input it came from is valid.

So lifetimes are not a way to control time. They are a way to describe trust.

Rust asks:

Can this reference still be trusted here?

Lifetime annotations help answer that question when the relationship is not obvious enough from the code.

When Rust Infers the Lifetime

Most of the time, you do not write lifetimes yourself.

Rust can infer simple borrowing patterns.

fn first_word(text: &str) -> &str {
    text.split_whitespace().next().unwrap_or("")
}

This function receives one borrowed string slice and returns a borrowed string slice from that same input.

Because there is only one input reference, Rust can infer the relationship.

It understands this shape:

input borrow -> output borrow

So you do not need to write this:

fn first_word<'a>(text: &'a str) -> &'a str {
    text.split_whitespace().next().unwrap_or("")
}

That explicit version is valid, but it is unnecessary here.

The compiler already knows the returned reference must come from text.

When You Need to Name the Relationship

Sometimes a function receives more than one borrowed value and returns one of them.

Now Rust needs to know how the output relates to the inputs.

fn display_name<'a>(username: &'a str, fallback: &'a str) -> &'a str {
    if username.is_empty() {
        fallback
    } else {
        username
    }
}

This function returns either username or fallback.

Both are borrowed inputs.

The lifetime annotation says:

The returned &str is tied to the same lifetime relationship as the borrowed inputs.

It does not mean both strings must live forever. It means the returned reference cannot be used longer than the data it came from.

Usage:

let username = String::from("andres");
let fallback = String::from("anonymous");

let shown = display_name(&username, &fallback);

println!("{shown}");

This is valid because both username and fallback are still alive when shown is used.

Borrowing Across Code Blocks

Code blocks matter because values are dropped at the end of the block where they are created.

This is valid:

let name = String::from("Andres");

{
    let borrowed = &name;
    println!("{borrowed}");
}

println!("{name}");

The borrowed reference lives inside the inner block. The owner, name, lives outside the inner block. So the borrow never outlives the owner.

That plain block also shows an important control-flow detail: a block can create scope and can produce a value, but it does not create a new ? boundary. If you use ? inside a plain block, it still returns from the surrounding function or closure. For that distinction, see Rust Option vs Result: ?, blocks, closures, and type flow.

But this shape is not valid:

let borrowed;

{
    let name = String::from("Andres");
    borrowed = &name;
}

println!("{borrowed}");

The problem is that name is created inside the block and destroyed at the end of the block.

After that, borrowed would point to a value that no longer exists.

Rust rejects this before the program can run.

The fix is to make the owner live long enough:

let name = String::from("Andres");
let borrowed;

{
    borrowed = &name;
}

println!("{borrowed}");

Now name still exists when borrowed is printed.

Returning a Borrow from a Collection

This is a common lifetime pattern: a function borrows a collection and returns a reference to one item inside it.

fn first_item<'a>(items: &'a [String]) -> &'a String {
    &items[0]
}

The function does not create a new String. It returns a reference to a String already inside the slice.

The lifetime annotation says:

The returned item reference is valid only while the borrowed slice is valid.

Usage:

let names = vec![String::from("Ana"), String::from("Bob")];

let first = first_item(&names);

println!("{first}");

This is valid because names is still alive when first is used.

The borrowed result depends on the borrowed input.

That relationship is what 'a describes.

Bridge from the Mutability Article

In the mutability article, we used this example:

fn longest_name<'a>(names: &'a [String]) -> &'a String {
    let mut longest: &String = &names[0];

    for name in &names[1..] {
        if name.len() > longest.len() {
            longest = name;
        }
    }

    longest
}

The important part is not that longest is mutable. That only means the local reference can point to a different item while the loop runs.

The lifetime part is this:

fn longest_name<'a>(names: &'a [String]) -> &'a String

It means:

I borrow a slice of names, and I return a borrowed name from inside that slice.

The function is not creating a new name. It is selecting one that already exists.

So Rust needs to know that the returned reference is tied to the input slice.

If the slice goes away, the selected name reference cannot be used anymore.

Structs That Store Borrowed Data

Functions are not the only place where lifetimes appear.

Structs need lifetime annotations when they store references.

struct UserView<'a> {
    name: &'a str,
}

This means UserView does not own the name. It only stores a borrowed reference to it.

Usage:

let name = String::from("Andres");

let view = UserView {
    name: &name,
};

println!("{}", view.name);

The struct cannot outlive the borrowed name.

Again, 'a does not keep name alive. It only tells Rust that UserView is tied to the borrowed data.

If you do not want this relationship, make the struct own the data instead:

struct OwnedUserView {
    name: String,
}

Owned data is often simpler. Borrowed data is useful when you want to avoid copying or when the struct is only a temporary view into data owned somewhere else.

Wrong Mental Models

It is easy to misunderstand lifetimes at first.

Here are some mental models to avoid.

Wrong: 'a makes the value live longer

It does not.

The owner decides how long the value lives. A lifetime annotation only describes how long a reference may be used safely.

Wrong: lifetimes are runtime timers

They are not.

Rust lifetimes are checked by the compiler. They are not counters, clocks, garbage collection handles, or runtime metadata.

Wrong: every reference needs explicit 'a

Most references do not need explicit lifetime annotations. Rust can infer many simple relationships.

You usually write lifetimes when references are stored or when a function returns a borrowed value related to one or more borrowed inputs.

TL;DR: Purpose-Driven Cases

Use the type of return or storage to decide what you need.

I only read borrowed data

Use a normal reference.

fn print_title(title: &str) {
    println!("{title}");
}

No explicit lifetime annotation is needed.

I create and return new owned data

Return an owned value.

fn make_title() -> String {
    String::from("Rust Notes")
}

No lifetime annotation is needed because the caller receives ownership.

I return a borrowed value from one borrowed input

Rust can often infer the lifetime.

fn first_word(text: &str) -> &str {
    text.split_whitespace().next().unwrap_or("")
}

I return a borrowed value from multiple borrowed inputs

You may need to name the relationship.

fn display_name<'a>(username: &'a str, fallback: &'a str) -> &'a str {
    if username.is_empty() {
        fallback
    } else {
        username
    }
}

I store references inside a struct

The struct usually needs a lifetime annotation.

struct UserView<'a> {
    name: &'a str,
}

I am confused by a lifetime error

Ask one question first:

Who owns this value?

Then ask:

Is this reference being used after the owner is gone?

That usually reveals the problem.

Conclusion

Borrowing means lending access to a value without transferring ownership.

Lifetimes are Rust’s way of making sure borrowed access does not outlive the owned value.

The key idea is:

'a does not extend a borrow. It names a relationship between borrowed values.

Use owned values when data needs to live independently.

Use references when data is owned somewhere else and you only need temporary access.

Use lifetime annotations when Rust needs help understanding how borrowed inputs and borrowed outputs are connected.

Rust is strict here because dangling references are dangerous. Instead of letting a program keep a broken pointer, Rust rejects the code before it runs.