| |

Rust 42 🦀 Lifetimes Concepts and Preventing Dangling References

Every reference in Rust has a lifetime. A lifetime is the scope for which a reference is valid, and it is the compiler’s mechanism for preventing dangling references — references that point to data that no longer exists. The borrow checker is the part of the compiler that tracks lifetimes, and its job is to prove that every reference is valid at every point where it is used. If it cannot prove this, the program does not compile. This is the guarantee that separates Rust from languages with garbage collection or manual memory management: no dangling pointers, no use-after-free, and no runtime cost for the safety.

Most lifetimes are elided. The compiler infers them from the code, and the developer never writes them. A function that takes a single reference and returns a reference gets the same lifetime for both, because the compiler assumes the output borrows from the input. A method that takes &self and returns a reference gets the lifetime of &self. These rules are called the lifetime elision rules, and they cover the majority of functions. The explicit lifetime syntax is only needed when the elision rules produce an ambiguity: when a function takes multiple references and returns a reference, the compiler cannot know which input the output borrows from. The explicit annotation resolves the ambiguity.

The distinction between lifetimes and scopes is important. A lifetime is not the same as the block in which a variable is declared. A lifetime is the region of code over which a reference is valid, and it is determined by the set of points where the reference may be used. The borrow checker uses a technique called non-lexical lifetimes (NLL) to compute the lifetime as the set of points where the reference is live, rather than the lexical block where it is declared. This means a reference can be considered dead before the end of the block if it is not used after a certain point, which allows more programs to compile.

This chapter covers three areas. First, why lifetimes exist — the dangling-reference problem and the borrow checker’s role. Second, the lifetime syntax and the elision rules — the 'a annotation, the elision rules for functions and methods, and the 'static lifetime. Third, lifetimes in structs, methods, and generic functions — the patterns for storing references in structs, the interaction with generics, and the common lifetime errors and their fixes. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the borrow checker’s analysis.

Key point: Every reference has a lifetime. The compiler infers most lifetimes with the elision rules, and the explicit 'a syntax is only needed when the elision is ambiguous. The borrow checker verifies that every reference is valid at every use. The 'static lifetime is the lifetime of the entire program. Lifetimes prevent dangling references, and they have no runtime cost.


Why lifetimes exist

The dangling-reference problem. A dangling reference is a reference to memory that has been freed. In C, a program can take a reference to a local variable, return it from a function, and use it after the function returns. The memory is deallocated, and the reference points to garbage. The behavior is undefined, and the bug is often silent. Rust’s lifetimes prevent this at compile time. The borrow checker verifies that the referenced data outlives the reference, and it rejects the program if it cannot prove this. The check happens once, at compile time, and it has no runtime cost.

The scope problem. In a language with garbage collection, the runtime tracks which objects are reachable and frees the unreachable ones. The programmer does not need to think about lifetimes, because the collector handles them. The cost is the runtime overhead of the collector and the unpredictability of the pauses. Rust’s lifetimes move the analysis to compile time. The programmer thinks about lifetimes, and the compiler verifies them. The cost is the mental effort of understanding the borrow checker, and the benefit is the absence of a runtime collector and the guarantee of no dangling references.

The aliasing problem. The borrow checker enforces two rules: a value can have either one mutable reference or any number of immutable references, but not both at the same time. This is the rule that prevents data races and iterator invalidation. The lifetimes are the mechanism that enforces the rule: an immutable reference’s lifetime cannot overlap with a mutable reference’s lifetime. The rule is the core of Rust’s safety guarantee, and the lifetimes are how it is checked.

The API problem. A function’s signature is a contract. If a function takes a reference and returns a reference, the caller needs to know how long the returned reference is valid. The lifetime annotation is the way to express this. fn first<'a>(s: &'a str) -> &'a str says that the returned reference is valid for as long as the input reference is valid. The caller can rely on this, and the compiler enforces it. Without the annotation, the contract would be ambiguous.

The struct problem. A struct that stores a reference must declare the lifetime of the reference. struct Excerpt<'a> { text: &'a str } says that the struct cannot outlive the string it borrows from. The compiler enforces this: the struct’s lifetime is bounded by the lifetime of the borrowed data. This is how a struct can hold a reference without the risk of a dangling pointer.

The trade-off. Lifetimes add complexity to function signatures. A function with multiple references and a returned reference needs an explicit annotation, and the annotation is not always obvious. The borrow checker rejects some programs that are actually safe, because the analysis is conservative. The 'static lifetime is often required where a shorter lifetime would suffice. The trade-off is between the safety guarantee and the expressiveness of the type system. For most programs, the safety is worth the complexity, and the elision rules keep the annotations minimal.


a. The lifetime syntax and the elision rules

A lifetime annotation is written with an apostrophe and a name, conventionally a single lowercase letter starting with 'a. The annotation is attached to a reference with &'a T. The annotation says that the reference is valid for the lifetime 'a.

fn first<'a>(s: &'a str) -> &'a str {
    &s[..1]
}

The function declares a lifetime parameter 'a, takes a reference with that lifetime, and returns a reference with the same lifetime. The contract is that the returned reference is valid for as long as the input reference is valid. The borrow checker verifies that the body satisfies this contract.

The lifetime elision rules allow the annotations to be omitted in common cases. There are three rules:

  1. Each input reference gets its own lifetime parameter.
  2. If there is exactly one input lifetime, it is assigned to all output lifetimes.
  3. If there is a &self or &mut self parameter, its lifetime is assigned to all output lifetimes.

The first rule means that fn f(x: &str, y: &str) is fn f<'a, 'b>(x: &'a str, y: &'b str). The second rule means that fn f(x: &str) -> &str is fn f<'a>(x: &'a str) -> &'a str. The third rule means that a method that takes &self and returns a reference gets the lifetime of &self.

The explicit annotation is needed when the elision rules do not apply. A function with two input references and a returned reference needs an annotation:

// ❌ Does not compile: which input does the output borrow from?
// fn longest(x: &str, y: &str) -> &str { ... }

// ✅ The annotation resolves the ambiguity
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

The longest function returns a reference that borrows from either x or y. The annotation says that both inputs and the output share the lifetime 'a, which means the returned reference is valid only for the intersection of the two input lifetimes. The caller must ensure that both inputs are valid for as long as the returned reference is used.

The 'static lifetime is the lifetime of the entire program. A reference with the 'static lifetime is valid for the entire execution. String literals have the 'static lifetime, because they are stored in the program’s binary.

let s: &'static str = "hello";

The 'static lifetime is also used as a trait bound: T: 'static means that the type T contains no references with a shorter lifetime. It does not mean that the value lives forever; it means that the value could live forever, because it owns all its data.


b. Lifetimes in structs and methods

A struct that holds a reference must declare a lifetime parameter. The parameter is used in the field’s type, and the struct’s lifetime is bounded by the referenced data.

struct Excerpt<'a> {
    text: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let excerpt = Excerpt { text: first_sentence };
    println!("{}", excerpt.text);
}

The Excerpt struct holds a reference to a string. The lifetime 'a says that the struct cannot outlive the string. The borrow checker enforces this: the excerpt variable is dropped before the novel variable, and the program compiles.

The struct with a lifetime can have methods. The method’s &self parameter has the struct’s lifetime, and the elision rules assign it to the output.

impl<'a> Excerpt<'a> {
    fn text(&self) -> &str {
        self.text
    }

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

The impl<'a> declares the lifetime for the implementation. The methods take &self, and the returned references have the lifetime of &self. The elision rules handle this automatically.

A struct can have multiple lifetime parameters if it holds references with different lifetimes. The annotations say which field has which lifetime.

struct Pair<'a, 'b> {
    first: &'a str,
    second: &'b str,
}

The Pair struct holds two references with potentially different lifetimes. The compiler tracks both and ensures that the struct does not outlive either.


c. Common lifetime errors and their fixes

The borrow checker’s errors are often about a reference that outlives the data it borrows. The error message names the lifetime and the conflict, and the fix is usually to adjust the lifetimes or the ownership.

The returned-reference error. A function returns a reference to a local variable, which is dropped when the function returns.

// ❌ Does not compile: returns a reference to a local
fn dangling() -> &String {
    let s = String::from("hello");
    &s
}

The fix is to return the owned value, not a reference:

// ✅ Returns the owned String
fn not_dangling() -> String {
    String::from("hello")
}

The multiple-input error. A function with two input references and a returned reference needs an annotation. The elision rules do not apply because there are two inputs.

// ❌ Does not compile: missing lifetime specifier
// fn longest(x: &str, y: &str) -> &str { ... }

// ✅ The annotation resolves the ambiguity
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

The struct-outlives-data error. A struct with a reference outlives the data it borrows.

// ❌ Does not compile: excerpt outlives novel
// let excerpt;
// {
//     let novel = String::from("...");
//     excerpt = Excerpt { text: &novel };
// }
// println!("{}", excerpt.text);

The fix is to ensure that the data outlives the struct. The novel must be declared in the outer scope, or the struct must own the data.

The mutable-borrow error. A mutable reference and an immutable reference overlap.

// ❌ Does not compile: cannot borrow as mutable while borrowed as immutable
// let mut v = vec![1, 2, 3];
// let first = &v[0];
// v.push(4);
// println!("{}", first);

The fix is to end the immutable borrow before the mutable borrow, or to clone the value. The non-lexical lifetimes rule often allows the immutable borrow to end earlier than the lexical block, but if the reference is used after the mutable borrow, the error remains.

The 'static bound error. A generic function requires T: 'static, but the caller passes a type with a shorter lifetime.

// ❌ Does not compile: borrowed data escapes
// fn spawn<F: Fn() + 'static>(f: F) { ... }
// let s = String::from("hello");
// spawn(|| println!("{}", s));

The fix is to move the data into the closure, so the closure owns it, or to use a different bound.


Complete Example Session

// ============================================
// PART 1: A DANGLING REFERENCE (WILL NOT COMPILE)
// ============================================

// fn dangling() -> &String {
//     let s = String::from("hello");
//     &s
// }
// error[E0106]: missing lifetime specifier

// The fix: return the owned value
fn not_dangling() -> String {
    String::from("hello")
}

fn main() {
    let s = not_dangling();
    println!("{}", s);
}


// ============================================
// PART 2: THE LIFETIME ANNOTATION
// ============================================

fn first<'a>(s: &'a str) -> &'a str {
    &s[..1]
}

fn main() {
    let s = String::from("hello");
    let f = first(&s);
    println!("{}", f); // h
}


// ============================================
// PART 3: THE ELISION RULES
// ============================================

// Elided: one input, one output
fn first_elided(s: &str) -> &str {
    &s[..1]
}

// The compiler expands it to:
fn first_explicit<'a>(s: &'a str) -> &'a str {
    &s[..1]
}


// ============================================
// PART 4: MULTIPLE INPUTS REQUIRE AN ANNOTATION
// ============================================

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("long string");
    let s2 = String::from("short");
    let result = longest(&s1, &s2);
    println!("{}", result); // long string
}

// Both inputs and the output share the lifetime 'a.
// The returned reference is valid for the shorter
// of the two input lifetimes.


// ============================================
// PART 5: THE 'static LIFETIME
// ============================================

fn static_str() -> &'static str {
    "hello"
}

fn main() {
    let s = static_str();
    println!("{}", s);
}

// String literals are stored in the binary,
// so they have the 'static lifetime.


// ============================================
// PART 6: A STRUCT WITH A LIFETIME
// ============================================

struct Excerpt<'a> {
    text: &'a str,
}

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

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let excerpt = Excerpt { text: first_sentence };

    println!("{}", excerpt.first_word()); // Call
}

// The Excerpt struct cannot outlive the novel.
// The borrow checker enforces this.


// ============================================
// PART 7: MULTIPLE LIFETIMES
// ============================================

struct Pair<'a, 'b> {
    first: &'a str,
    second: &'b str,
}

fn main() {
    let s1 = String::from("hello");
    let s2 = String::from("world");
    let pair = Pair { first: &s1, second: &s2 };

    println!("{} {}", pair.first, pair.second);
}


// ============================================
// PART 8: A METHOD WITH A LIFETIME
// ============================================

struct Container<'a> {
    value: &'a str,
}

impl<'a> Container<'a> {
    fn get(&self) -> &str {
        self.value
    }

    fn longest<'b>(&'a self, other: &'b str) -> &'a str
    where
        'b: 'a,
    {
        if self.value.len() > other.len() {
            self.value
        } else {
            self.value
        }
    }
}


// ============================================
// PART 9: THE MUTABLE-BORROW ERROR
// ============================================

fn main() {
    let mut v = vec![1, 2, 3];

    let first = &v[0];
    println!("{}", first);
    // The immutable borrow ends here.

    v.push(4);
    // The mutable borrow is now allowed.

    println!("{:?}", v);
}

// Non-lexical lifetimes allow the immutable borrow
// to end before the end of the block.
// The mutable borrow is allowed after the last use
// of the immutable borrow.


// ============================================
// PART 10: THE COMPLETE EXAMPLE
// ============================================

fn longest_with_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: std::fmt::Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("long string");
    let s2 = String::from("short");

    let result = longest_with_announcement(&s1, &s2, "Comparing strings");
    println!("The longest is: {}", result);
}

The ten parts show the dangling reference, the lifetime annotation, the elision rules, multiple inputs, the 'static lifetime, a struct with a lifetime, multiple lifetimes, a method with a lifetime, the mutable-borrow error, and the complete example.


Quick Reference

Lifetime Syntax

FormMeaning
&'a TA reference with lifetime 'a
fn f<'a>(x: &'a T) -> &'a TLifetime parameter and bounds
struct S<'a> { f: &'a T }A struct with a lifetime
impl<'a> S<'a> { ... }An impl block with a lifetime
'staticThe lifetime of the entire program
T: 'aThe type T outlives 'a

The Elision Rules

RuleEffect
1Each input reference gets its own lifetime
2One input lifetime → assigned to the output
3&self lifetime → assigned to the output

Common Signatures

SignatureMeaning
fn f(x: &str) -> &strElided: same lifetime
fn f<'a>(x: &'a str, y: &'a str) -> &'a strBoth inputs and output share 'a
fn f<'a>(x: &'a str) -> &'static strOutput has 'static lifetime
impl<'a> S<'a> { fn m(&self) -> &str }&self lifetime assigned to output

Lifetime Errors

ErrorCauseFix
Missing lifetime specifierMultiple inputs, returned referenceAdd an annotation
Returns a reference to a localDangling referenceReturn the owned value
Borrowed value does not live long enoughReference outlives the dataExtend the data’s scope or own it
Cannot borrow as mutableOverlapping borrowsEnd the immutable borrow earlier
'static bound not satisfiedBorrowed data escapesMove the data into the closure

Best Practices

✅ Do This:

// Let the elision rules handle simple cases
fn first(s: &str) -> &str { &s[..1] }                                     // ✅
// Add annotations when there are multiple inputs
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }                 // ✅
// Use 'static for string literals
fn greeting() -> &'static str { "hello" }                                 // ✅
// Declare the lifetime in a struct that holds a reference
struct Excerpt<'a> { text: &'a str }                                      // ✅
// Use non-lexical lifetimes to shorten borrows
let first = &v[0]; println!("{}", first); v.push(4);                      // ✅
// Prefer owned types when the lifetime is complex
fn make() -> String { String::from("hello") }                             // ✅

❌ Don’t Do This:

// Don't return a reference to a local
fn dangling() -> &String { let s = String::new(); &s }                    // ❌
// Don't omit the annotation with multiple inputs
fn longest(x: &str, y: &str) -> &str { ... } // missing lifetime          // ❌
// Don't fight the borrow checker with unsafe
let r = unsafe { &*ptr }; // only when justified                           // ❌
// Don't use 'static when a shorter lifetime suffices
fn f() -> &'static str { &String::from("hi") } // does not compile        // ❌
// Don't clone to avoid lifetimes without reason
let s = data.clone(); // may be expensive                                 // ❌

Common Pitfalls

PitfallWhy It HappensFix
Missing lifetime specifierMultiple input referencesAdd the annotation
Returns a reference to a localDangling referenceReturn an owned value
Borrowed value does not live long enoughReference outlives the dataExtend the scope or own the data
Cannot borrow as mutableOverlapping borrowsEnd the immutable borrow earlier
'static bound not satisfiedBorrowed data escapesMove the data into the closure
Struct outlives the dataThe struct’s lifetime is longerDeclare the data in an outer scope
Lifetime mismatch in a method&self and the parameter have different lifetimesAdd explicit bounds

Real-World Examples

1. Elided Lifetime

fn first(s: &str) -> &str { &s[..1] }

2. Explicit Lifetime

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }

3. ‘static

fn greeting() -> &'static str { "hello" }

4. Struct with a Lifetime

struct Excerpt<'a> { text: &'a str }

5. Impl Block with a Lifetime

impl<'a> Excerpt<'a> { fn text(&self) -> &str { self.text } }

6. Multiple Lifetimes

struct Pair<'a, 'b> { first: &'a str, second: &'b str }

7. Method with a Lifetime

impl<'a> S<'a> { fn get(&self) -> &str { self.value } }

8. Lifetime Bound

fn f<'a, T: 'a>(x: &'a T) -> &'a T { x }

9. Non-Lexical Lifetimes

let first = &v[0]; println!("{}", first); v.push(4);

10. Complete Example

fn longest_with_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where T: std::fmt::Display { ... }

Visual

The Borrow Checker’s Analysis

┌──────────────────────────────────────────────────────────────┐
│  THE BORROW CHECKER'S ANALYSIS                               │
│                                                              │
│  fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {         │
│    if x.len() > y.len() { x } else { y }                     │
│  }                                                           │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  'a: the lifetime of both inputs and the output      │    │
│  │                                                       │    │
│  │  ┌──────────────┐                                    │    │
│  │  │  x: &'a str  │ ──┐                                │    │
│  │  └──────────────┘   │                                │    │
│  │                     ├──► return: &'a str             │    │
│  │  ┌──────────────┐   │                                │    │
│  │  │  y: &'a str  │ ──┘                                │    │
│  │  └──────────────┘                                    │    │
│  │                                                       │    │
│  │  The returned reference is valid for the             │    │
│  │  SHORTER of the two input lifetimes.                 │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  Caller:                                                     │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  let s1 = String::from("long string");               │    │
│  │  let result;                                         │    │
│  │  {                                                   │    │
│  │    let s2 = String::from("short");                   │    │
│  │    result = longest(&s1, &s2);                       │    │
│  │  }                                                   │    │
│  │  println!("{}", result);  ← ERROR: s2 is dropped     │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The borrow checker rejects the program because the          │
│  returned reference would outlive s2.                        │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Elision Rules

┌──────────────────────────────────────────────────────────────┐
│  THE ELISION RULES                                           │
│                                                              │
│  RULE 1: Each input reference gets its own lifetime.         │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  fn f(x: &str, y: &str)                              │    │
│  │  → fn f<'a, 'b>(x: &'a str, y: &'b str)              │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  RULE 2: One input lifetime → assigned to the output.        │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  fn f(x: &str) -> &str                               │    │
│  │  → fn f<'a>(x: &'a str) -> &'a str                   │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  RULE 3: &self lifetime → assigned to the output.            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  impl S { fn m(&self) -> &str }                      │    │
│  │  → impl S { fn m<'a>(&'a self) -> &'a str }          │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The rules cover most functions. The explicit annotation     │
│  is needed when the elision is ambiguous — when there are    │
│  multiple inputs and a returned reference, and the compiler  │
│  cannot know which input the output borrows from.            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

A Struct with a Lifetime

┌──────────────────────────────────────────────────────────────┐
│  A STRUCT WITH A LIFETIME                                    │
│                                                              │
│  struct Excerpt<'a> {                                        │
│    text: &'a str,                                            │
│  }                                                           │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  'a: the lifetime of the borrowed string             │    │
│  │                                                       │    │
│  │  The struct cannot outlive the string.               │    │
│  │  The borrow checker enforces this.                   │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  fn main() {                                                 │
│    let novel = String::from("Call me Ishmael...");           │
│    let first = novel.split('.').next().unwrap();             │
│    let excerpt = Excerpt { text: first };                    │
│    println!("{}", excerpt.text);                             │
│  }                                                           │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  novel:   ──────────────────────────────────────►    │    │
│  │  excerpt: ────────────────►                          │    │
│  │                                                       │    │
│  │  excerpt is dropped before novel.                    │    │
│  │  The program compiles.                               │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Non-Lexical Lifetimes

┌──────────────────────────────────────────────────────────────┐
│  NON-LEXICAL LIFETIMES                                       │
│                                                              │
│  BEFORE NLL (lexical):                                       │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  let mut v = vec![1, 2, 3];                          │    │
│  │  let first = &v[0];   ← immutable borrow starts      │    │
│  │  println!("{}", first);                              │    │
│  │  v.push(4);           ← ERROR: still borrowed        │    │
│  │                                                       │    │
│  │  The borrow lasts until the end of the block.        │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  WITH NLL (non-lexical):                                     │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  let mut v = vec![1, 2, 3];                          │    │
│  │  let first = &v[0];   ← immutable borrow starts      │    │
│  │  println!("{}", first);                              │    │
│  │  v.push(4);           ← OK: borrow ended            │    │
│  │                                                       │    │
│  │  The borrow ends at the last use of first.           │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  NLL allows more programs to compile by computing the        │
│  lifetime as the set of points where the reference is live,  │
│  not the lexical block where it is declared.                 │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
LifetimeThe scope for which a reference is valid
Annotation&'a T
Elision rulesThree rules that infer lifetimes
'staticThe lifetime of the entire program
Struct with a lifetimestruct S<'a> { f: &'a T }
Impl with a lifetimeimpl<'a> S<'a> { ... }
Lifetime boundT: 'a
Non-lexical lifetimesThe lifetime is the set of live points
Borrow checkerVerifies every reference at every use
Dangling referenceA reference to freed memory; prevented at compile time

Key takeaways:

  • Every reference has a lifetime. The lifetime is the scope for which the reference is valid, and the borrow checker verifies that every reference is valid at every point where it is used. The check happens at compile time and has no runtime cost.
  • Most lifetimes are elided. The compiler infers them from the code using the three elision rules. The explicit annotation is only needed when the elision is ambiguous — when a function takes multiple references and returns a reference.
  • The elision rules are: one lifetime per input, one input lifetime assigned to the output, and &self assigned to the output. The rules cover the majority of functions, and the explicit annotation is the exception.
  • A struct that holds a reference must declare a lifetime. The lifetime says that the struct cannot outlive the borrowed data. The borrow checker enforces this, and it prevents the struct from holding a dangling reference.
  • The 'static lifetime is the lifetime of the entire program. String literals have the 'static lifetime. A T: 'static bound means that the type contains no references with a shorter lifetime; it does not mean that the value lives forever.
  • Lifetimes prevent dangling references. A function that returns a reference to a local variable does not compile. The fix is to return an owned value or to extend the scope of the data.
  • The borrow checker rejects some safe programs. The analysis is conservative, and it may reject a program that is actually safe. The fix is usually to adjust the lifetimes, to clone the data, or to restructure the code.
  • Non-lexical lifetimes shorten the borrow’s lifetime. The lifetime is the set of points where the reference is live, not the lexical block where it is declared. This allows a mutable borrow after the last use of an immutable borrow, and it makes more programs compile.

Remember: Lifetimes are the mechanism that prevents dangling references in Rust. Every reference has a lifetime, and the borrow checker verifies that the reference is valid at every point where it is used. The compiler infers most lifetimes with the elision rules, and the explicit annotation is only needed when the elision is ambiguous. A struct that holds a reference declares a lifetime, and the 'static lifetime is the lifetime of the entire program. The borrow checker rejects the program if it cannot prove that every reference is valid, and the rejection is a compile-time error, not a runtime crash. The result is memory safety without a garbage collector, and the cost is the mental effort of understanding the borrow checker.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!