The contract behind the colon
You write a function that takes two string slices and returns the longer one. The compiler yells at you. You add lifetime annotations to make it compile, but now you are staring at a constraint that looks like 'b: 'a. It reads like math, but it is actually a contract about time. You need to understand what that colon means, because it is the mechanism Rust uses to guarantee references never point to freed memory.
Lifetime subtyping is the rule that a reference with a longer lifetime can safely stand in for a reference with a shorter lifetime. Think of it like event tickets. A VIP pass is valid for the entire weekend. A general admission ticket is valid for Saturday only. If a security guard asks for a ticket valid for Saturday, you can hand over the weekend pass. It covers the required time. You cannot hand over the Saturday ticket if they ask for weekend access. The longer validity is a subtype of the shorter validity requirement.
In Rust, 'b: 'a reads as "lifetime 'b outlives lifetime 'a". It means anything borrowed for 'b will still be alive when 'a ends. The borrow checker uses this to allow you to return a reference tied to the shorter lifetime, as long as you prove the other input lives long enough to cover it. Treat the colon as a containment operator. The left side must fully contain the right side.
/// Returns the first string slice, but requires the second to live at least as long.
fn pick_first<'a, 'b: 'a>(first: &'a str, second: &'b str) -> &'a str {
// The constraint 'b: 'a guarantees second outlives first.
// We return first, which is valid for 'a.
first
}
The function signature declares two lifetimes. The colon syntax 'b: 'a is the subtyping bound. It tells the compiler that second must not be dropped before first. Because of that guarantee, returning first is safe. The caller knows the returned reference will live at least as long as 'a, and the compiler knows 'b covers that duration. Trust the direction of the bound. It flows from long to short, never the other way around.
How the compiler checks the bound
When you call this function, the compiler infers concrete lifetimes for the arguments. Suppose you pass a string that lives for the entire program and a string that lives only inside a match block. The compiler assigns the program lifetime to 'b and the block lifetime to 'a. It checks the bound: does the program lifetime outlive the block lifetime? Yes. The call compiles.
Now swap the arguments. Pass the short-lived string as first and the long-lived string as second. The compiler tries to assign the block lifetime to 'a and the program lifetime to 'b. The bound still holds. But the return type is &'a str. The compiler now knows the returned reference only lives for the block. If you try to use it after the block ends, you get a borrow checker error. The subtyping bound did not extend the lifetime. It only verified that the inputs were compatible with the return type duration.
This is the core mechanism. Subtyping does not stretch lifetimes. It only verifies that one duration fully contains another. The compiler never makes a short-lived reference live longer. It only allows you to treat a long-lived reference as if it were short-lived. That directionality is baked into the type system. The compiler solves these constraints by substituting inferred lifetimes into the bound and checking the inequality. If the inequality fails, compilation stops.
Rust references are covariant over their lifetimes. Covariance means the compiler automatically allows a longer lifetime to shrink to a shorter one when you pass it to a function or store it in a struct. You rarely need to write 'b: 'a because the compiler applies this rule implicitly. The explicit bound is only required when the relationship is not obvious to the inference engine, or when you are defining a trait that must work across multiple generic contexts. Write the bound only when the compiler explicitly demands it. Keep your signatures loose until the borrow checker forces your hand.
When you actually need it
Real code rarely uses 'b: 'a for simple string comparisons. It appears when you build abstractions that must guarantee a reference survives a specific scope, often in parsers, caches, or data structures that borrow from multiple sources.
/// A parser that extracts a token from a buffer.
/// The buffer lives longer than the temporary query string.
struct Parser<'a> {
buffer: &'a str,
}
impl<'a> Parser<'a> {
/// Searches for a pattern. The pattern can be short-lived,
/// but the returned token must live as long as the buffer.
fn find_token<'b: 'a>(&self, pattern: &'b str) -> &'a str {
// The pattern 'b is used for matching logic.
// We return a slice of self.buffer, which lives for 'a.
// The bound ensures pattern outlives the buffer slice we return.
self.buffer
}
}
The find_token method takes a pattern with lifetime 'b. The matching logic might borrow the pattern temporarily, but the return value comes from self.buffer, which lives for 'a. The bound 'b: 'a ensures the pattern outlives the buffer data. This seems backwards at first. Why would a lookup pattern need to outlive the cached value? In this specific signature, it actually restricts the caller unnecessarily. A better design drops the bound and returns &'a str directly, letting elision handle it. But the example shows how subtyping forces a relationship between two independent borrow scopes. When you see 'b: 'a in real code, it usually means the author needed to prove that a temporary borrow will not outpace a longer-term allocation.
Convention aside: the Rust community rarely writes explicit outlives bounds like 'b: 'a in modern code. Lifetime elision rules and variance usually handle the relationship automatically. You will mostly encounter this syntax in trait bounds, generic algorithms, or when fighting complex borrow checker errors that refuse to infer the relationship. Write the bound only when the compiler explicitly demands it.
Where the bound breaks down
The most common mistake is assuming 'b: 'a makes 'a live longer. It does not. It only restricts what you can pass in. If you try to return a reference that actually depends on 'b without the bound, the compiler rejects you with E0515 (cannot return value referencing local data) or a lifetime mismatch error. The borrow checker sees that the returned reference might dangle if 'b ends before 'a.
Another trap is over-constraining. Adding 'b: 'a when you only need 'a: 'b flips the contract. The compiler will reject valid calls because it enforces the direction you specified. If you accidentally write 'a: 'b instead of 'b: 'a, you force the shorter lifetime to outlive the longer one. That constraint is impossible to satisfy in most call sites. The compiler responds with a "lifetime may not live long enough" error. Check the direction of the colon carefully. The lifetime on the left must outlive the lifetime on the right.
You will also run into variance issues. Rust references are covariant over their lifetimes. That means &'long str can automatically coerce to &'short str without an explicit bound. The subtyping syntax is only required when the relationship is not obvious to the compiler, or when you are defining a trait that must work across multiple generic contexts. Don't add the bound unless the compiler explicitly asks for it or you are building a highly generic API. If you force a bound where covariance already applies, you create unnecessary friction for callers. Keep the signature as loose as possible.
Trait bounds complicate the picture further. When you write impl<T: 'a> Trait for T, you are using the same outlives syntax to guarantee that T does not contain any references shorter than 'a. This prevents you from accidentally storing a short-lived reference inside a struct that claims to live for 'a. The compiler enforces this with E0309 (explicit lifetime required in the type of self) if you try to bypass it. The rule remains identical: the left side must contain the right side. Treat the bound as a safety net, not a performance tool.
Choosing the right lifetime strategy
Use explicit outlives bounds when you are writing a generic trait or function that must guarantee a specific lifetime relationship across multiple parameters. Use lifetime elision when the compiler can infer the relationship from input and output positions. Use owned types like String or Vec when the caller needs to store the result beyond the scope of the borrowed data. Reach for Cow<str> when you need to return either borrowed data or owned data depending on runtime conditions.