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: &String) {
    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.

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.

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.