Rust Lifetimes

Rust lifetimes and lifetime annotation

In Rust, the term lifetime is used to describe how long a reference remains valid, based on the scope of the value being borrowed. References are susceptible to memory safety issues, so Rust allows the programmer to include information about reference lifetimes using lifetime annotations.

Lifetime annotations help us to resolve compiler errors while maintaining a high level of memory safety when working with references.

This article covers the topics of lifetimes and lifetime annotations in the Rust programming language.

Ownership, Borrowing, and Lifetimes in Rust

The term ‘lifetime’ generally refers to how long a value is stored in memory, i.e. a value’s longevity. A variable’s lifetime begins when it is declared, and ends when it goes out of scope.

We’ve seen that Rust can be quite strict about ownership because the ownership system is how Rust achieves memory safety.

For example, the strict rules around copy and move types prevent multiple pointers to the same data (i.e. a data race). Borrowing is designed to help alleviate the difficulty of coding with these restrictions by allowing use references instead of transferring ownership.

However borrowing can lead to other memory safety issues, a topic discussed in more detail below. This is where lifetimes and lifetime annotations become useful.

What Are Lifetimes?

The term ‘lifetime’ refers to the life cycle of data stored in memory. Lifetime is determined by scope; when a variable goes out of scope, the memory it uses is cleared automatically. So the lifetime of any variable starts when it is first declared, and ends when it goes out of scope.

When a variable is copied or moved, the new variable has a scope that is determined like any other variable. But references are different, because they point to an address in memory without taking ownership of the data that lives there. This can lead to issues such as the infamous dangling pointer, also called ‘use after free’.

What happens when a reference calls data that has already been cleared from memory? Take the following example:

let x; //x has an outer scope

{
    let y = 42; // y has an inner scope
    x = &y;
} // y goes out of scope here

println!("x: {}", x); // error because y out of scope

Standard Error:

error[E0597]: `y` does not live long enough
 --> src/main.rs:5:13
  |
5 |         x = &y;
  |             ^^ borrowed value does not live long enough
6 |     }
  |     - `y` dropped here while still borrowed
7 |     
8 |     println!("x: {}", x);
  |                       - borrow later used here

In this case, the println! macro calls x, which is bound to a reference of the variable y. But x and y have different scopes! The value assigned to y (42), gets cleared from memory when y reaches the end of its’ lifetime (when y goes out of scope). x is now pointing to something in memory that no longer exists, and the compiler won’t allow it.

In this case, fixing the issue is easy enough. We can just make sure that the scope of the borrowed value is the same as that of the borrow:

let x;
let y = 42;
x = &y;

println!("x: {}", x);

Standard Output:

x: 42

Of course this is a trivial case that was easily resolved by removing the artificial inner-scoped code block.

What about more complex examples, like functions with multiple reference parameters? That’s where lifetime annotations come in.

Lifetime Annotations

Lifetime annotations allow us to specify the lifetime of a value in order to avoid conflict due to references.

Lifetime annotation syntax uses an apostrophe: <‘a>, &’a, <‘b>, &’b, etc. Before we get into the details of using lifetime annotations, let’s learn a bit more about them and how they work.

The following function does not require any lifetime annotations:

fn my_func(x: &i32) -> &i32 {}

Here’s a working example:

let x = 42;
println!("{}", my_func(&x));

fn my_func(num: &i32) -> &i32 {
    num
}

Standard Output:

42

This example compiles because there is only one reference input. In this case, the compiler will be able to infer that the lifetime of the function return is the same as the lifetime of the argument.

If we add a second reference parameter, this code will not compile! That’s because there can be ambiguity between the two references. For example, each reference may not have the same scope.

Take the following case:

let red = "Red";
let green = "Green";

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
let longer_string = longest(red, green);
println!("{}", longer_string);

Standard Error:

4 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
4 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

This code throws an error because there are two input references. They each have an implied scope but at compile time, there may be ambiguity between them. We can resolve this issue with lifetime annotations.

Both arguments have references with the same scope, so why is there an error?

The compiler tries to protect us not only from potential memory safety issues, but even from the possibility of a memory safety issues. In other words, Rust wants to prevent us from potentially doing something unsafe (even when what we’re actually doing is safe). That’s why the compiler throws an error even when the default lifetimes of the referenced values have the same scope. When we give a function two reference parameters, Rust will always require lifetime annotations.

What Are Lifetime Annotations?

Lifetime annotations allow us to give the compiler information about lifetimes so that it can check the validity of the references. They are descriptive rather than prescriptive, meaning that they don’t redefine scope or lifetime. Instead, lifetime parameters help to resolve ambiguity in situations that throw an error, like having multiple references as parameters.

Lifetime annotation syntax uses an apostrophe: <‘a>, &’a, <‘b>, &’b, etc. The actual name given to the lifetime annotation can be anything (i.e. <‘lifetime1> is acceptable) but it is customary to use lowercase letters starting with a, i.e. <a>.

Let’s see how lifetime annotation can be used to resolve the issue in our previous code:

let red = "Red";
let green = "Green";

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

Standard Output:

Green

To resolve the issue, we did two things:

  • We added a lifetime annotation following the function name, in between two angled brackets:

fn function_name<‘a>() {}

  • We also included lifetime annotations for each parameter as well as the returned reference using &’a:

fn function_name<‘a> (x: &’a str, y: &’a str) -> &’a str {}

Important: Lifetime annotations are descriptive, not prescriptive. They don’t assign a new lifetime to a variable; the lifetime of a variable is always determined by scope. What they do is prevent uncertainty regarding the lifetime of variables.

Multiple Lifetimes

Situations involving multiple lifetimes can be resolved using multiple lifetime annotations. It is customary to denote different lifetimes using letters starting with a.

In the following example, we have a function that takes in three reference parameters, each with a different lifetime. We want the function’s return value to have the same lifetime as the second reference parameter, so we annotate this using &’b:

fn function_name<'a> (x: &'a str, y: &'b str, z: &'c str) -> &'b str {}

Now that we’ve seen a few examples of how lifetime annotations work, let’s dive a bit deeper into where lifetime annotations are needed and when they aren’t.

When are lifetime annotations not needed?

  • When references are not used – lifetime annotations are only needed when we’re dealing with references.
  • When only one reference is passed in to a function and no reference or a single reference is returned. In this case, the compiler will assume that the lifetime of the returned value is the same as the lifetime of the argument.
  • When multiple references are passed into a function and a reference is returned but the arguments all have the same scope. In this case there is no ambiguity and the compiler will assume

When are lifetime annotations needed?

  • Functions with multiple references as parameters: When a function takes in multiple references as parameters, the compiler needs to know how long these references should be valid. This is specified using lifetime annotations.
  • Structs that contain references as fields: If a struct has one or more references as fields, the compiler needs to know how long these references should be valid.
  • Trait bounds on references: When working with traits that operate on references. We haven’t covered traits yet but this topic will be addressed in more detail later in this course.
  • Closures that capture references: Closures in Rust can capture references to data that is outside of their scope. In these cases, the lifetime of these captured references needs to be specified using lifetime annotations.

Lifetimes arise due of the conflicts that can occur because a binding can have only one owner, but there can be multiple references to that value. The scope of the binding is determined by where it is defined in the code (following Rust’s rules for scope). But references can be defined in a different scope and if used improperly, they can point to an invalid resource.

Lifetimes are used to ensure that memory isn’t cleared before we need it.



Lifetimes always deal with references and are designed to eliminate potential bugs that are associated with references. This is because a reference may have a different scope than the value it is referencing, which can cause a variety of issues.