When C developers first see lifetimes, it is easy to think they are annotations for “how long an object lives.”
That is not quite right.
Rust lifetimes are not runtime timers, and they are not manual descriptions of real object lifetime. They are static constraints the compiler uses to check reference relationships: a reference cannot outlive the data it points to.
C has the same problem. It is just checked by humans.
Dangling Pointers in C
The classic example is returning the address of a local variable:
const char *bad_name(void)
{
char buf[32] = "sensor";
return buf;
}
buf is gone when the function returns. The returned pointer points to invalid stack storage.
Similar issues include:
- returning the address of a local array
- storing a pointer to a caller’s temporary buffer
- using a pointer after
free - using an old address after
realloc - putting a pointer to short-lived data into a long-lived struct
In C, these bugs do not always crash immediately. They may appear after another function call, stack overwrite, or heap allocation.
Rust lifetimes prevent these reference relationship errors.
Rust Rejects Returning Local References
The Rust version does not compile:
fn bad_name() -> &str {
let name = String::from("sensor");
&name
}
The problem is not String itself or &str itself. The returned reference points into name. When the function ends, name is released, so the returned reference would dangle.
The compiler sees this relationship:
return reference -> points into local variable name
name -> released when function ends
return reference -> used outside the function
That relationship is invalid, so the code cannot compile.
This is the first meaning of lifetimes: check whether referenced data is still alive.
Most Lifetimes Are Inferred
Many functions do not need lifetime annotations:
fn first_byte(data: &[u8]) -> Option<u8> {
data.first().copied()
}
This returns a u8, not a reference. The function borrows data internally but does not return a reference.
Now:
fn trim_name(name: &str) -> &str {
name.trim()
}
The returned &str comes from the input name. The compiler can infer that the returned reference cannot outlive the input reference.
So this is not needed:
fn trim_name<'a>(name: &'a str) -> &'a str {
name.trim()
}
Both signatures express the same relationship. The explicit lifetime only writes out what the compiler can infer.
Lifetime Annotations Describe Reference Relationships
When are annotations needed? Usually when a function signature has multiple references and the compiler cannot know which input the returned reference comes from.
Example:
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() {
a
} else {
b
}
}
'a does not mean a and b literally live for the same amount of time. It expresses a constraint:
the returned reference may only be used while both a and b are still valid
Because the returned reference may come from either a or b, the caller cannot use it longer than the shorter valid input.
This is close to a C documentation rule:
/* returns a pointer into either a or b; caller must keep both alive */
const char *longer(const char *a, const char *b);
Rust puts that rule into the type.
Structs Holding References Need the Relationship
C structs often store external pointers:
struct View {
const uint8_t *data;
size_t len;
};
This struct does not own data. It is only a view. The pointed-to buffer must outlive the View.
In Rust, a struct holding a reference needs a lifetime parameter:
struct View<'a> {
data: &'a [u8],
}
'a means:
View cannot outlive the data it borrows
It does not make View own the data. If the struct should own data, use Vec<u8>:
struct OwnedBuffer {
data: Vec<u8>,
}
The API boundary matters:
stored borrow: struct View<'a> { data: &'a [u8] }
owned data: struct OwnedBuffer { data: Vec<u8> }
If you do not want callers to deal with lifetimes, often the struct should own the data instead of storing references.
‘static Is Not a Magic Fix
'static is often misunderstood.
String literals have a 'static lifetime:
let name: &'static str = "sensor";
The string data is stored in the program’s static region and can live until program exit.
But 'static is not a magic way to fix lifetime errors. A function cannot force a reference to a local variable to become 'static:
fn bad() -> &'static str {
let s = String::from("sensor");
&s
}
This is still invalid. Local s is released when the function ends, so its reference cannot be 'static.
When you see a lifetime error, the right question is usually not “what annotation should I add?” It is:
who owns the data this reference points to?
should the return value or struct own the data instead?
is this reference being stored too long?
What to Keep from This Article
The first mapping is:
return address of local variable -> Rust rejects dangling reference
pointer into caller buffer -> reference cannot outlive caller data
struct stores external pointer -> struct View<'a>
struct owns its own buffer -> struct Owned { data: Vec<u8> }
documentation says caller keeps alive -> lifetime relationship in type
Lifetimes are not real object lifetimes, and they are not handwritten lifespans for variables.
They express reference relationships: how long a reference may be used depends on how long the data it points to remains valid.
The next article can move to enum and pattern matching: from C integer state codes, macro constants, and switch to Rust types that express states and error branches.