Rust Basics #9 Lifetimes and Modules: When Lifetime Annotations Are Needed, Finishing with Crates
The final part. One promise remains: lifetimes, which Part 4 described only as “the check that decides how long a reference stays valid.” After settling it, we leave the single file behind — modules and external crates structure a real project — and close the course.
Lifetimes: the compiler was computing them all along #
Part 4 said that returning a reference to a local variable is rejected. The basis of that verdict is the lifetime. For every reference, the compiler computes “the span where the referenced value is alive” and “the span where the reference is used,” and rejects the program when the latter escapes the former. This computation has been running on every reference we ever wrote; we simply never annotated anything.
The classic moment when annotations become necessary is a function that takes several references and returns one.
fn longer(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}error[E0106]: missing lifetime specifier
|
1 | fn longer(a: &str, b: &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 `a` or `b`The message states the reason precisely: the signature alone cannot tell whether the returned reference came from a or from b. The caller needs that information to judge how long the return value may be used. The answer is a lifetime parameter.
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}'a looks intimidating at first sight, but its meaning is one sentence: “the returned reference cannot outlive whichever of a and b lives shorter.” A lifetime annotation is not magic that makes values live longer — it is documentation of a relationship that already exists, addressed to the compiler and the caller. And that relationship is exactly what makes the following code fail, correctly:
let result;
{
let b = String::from("short-lived string");
result = longer("long-lived one", &b);
} // b is freed here
println!("{result}"); // compile error: tried to outlive b
The good news: writing lifetime annotations by hand is rarer than you might fear. Where the relationship is self-evident — a function taking one reference and returning one — the compiler fills it in through the elision rules. That is why Part 4’s count_chars(text: &str) carried no annotation. Structs holding references also require lifetime annotations, but the practical guideline at the basics stage is simple: default struct fields to owned types (String) rather than references (&str), and treat spreading lifetime annotations as a signal to reconsider the design.
Modules: leaving the single file #
The whole course lived in main.rs, but real projects split their code. Rust’s unit is the module.
// src/billing.rs
pub struct Invoice { pub amount: u32 }
pub fn issue(amount: u32) -> Invoice {
log_issue(amount);
Invoice { amount }
}
fn log_issue(amount: u32) { /* module-private */ }// src/main.rs
mod billing; // include src/billing.rs as a module
use billing::issue;
fn main() {
let invoice = issue(50_000);
println!("Invoice amount: {}", invoice.amount);
}Three rules cover it. mod billing; wires the file in as a module; everything is private by default, so pub goes only on what the outside should see; and use shortens paths. Just as variables were immutable by default, visibility is private by default — the language pushes in the same direction, making the public API an explicit choice.
External crates: one line of cargo add #
Rust’s package unit is called a crate, and the public registry is crates.io. Dependency management belongs to cargo, via the Cargo.toml we created in Part 1.
cargo add randuse rand::Rng;
fn main() {
let dice = rand::rng().random_range(1..=6);
println!("dice roll: {dice}");
}cargo add records the dependency in Cargo.toml; the next build downloads and compiles it. Exact versions are pinned in Cargo.lock, so the whole team reproduces the same build. The setup mirrors Python’s requirements/lock or JavaScript’s package.json/lock — except it is built into the language from the start, with no fragmented tooling, which is what “cargo is everything” meant back in Part 1. As for which crates are de facto standard: knowing the names serde for serialization, reqwest for HTTP clients, and tokio for async runtimes will make searching much easier.
Closing the course: what comes next #
The nine-part journey in summary: Rust earns memory safety without a GC through ownership (Part 3) and borrowing (Part 4), forces “absence” and “failure” through the type system with enums, Option and Result (Parts 5–6), abstracts with traits (Part 8), and proves the validity range of references with lifetimes (this part). All of it is a variation on one principle: pull runtime accidents forward into compile errors.
Three recommendations for the road ahead. Fill in what this course dug less deeply — closures in depth, smart pointers, concurrency — with the official book, The Rust Programming Language (known as The Book); raise your volume of conversation with the compiler through the exercise collection rustlings; then build one small CLI tool end to end. Rust is a language where the gap between having read about it and having pushed code through the borrow checker is unusually wide.
Summary #
- A lifetime is the “validity span” the compiler always computes for every reference. Annotation is needed only where the relationship turns ambiguous — canonically, a function taking several references and returning one.
'adoes not extend anything; it declares “the returned reference cannot outlive the shorter-lived input.” Elision rules fill in most cases.- Struct fields default to owned types. Spreading lifetime annotations are a signal to revisit the design.
- Modules wire in with
mod, private by default,pubonly on the public surface. External crates are onecargo add, with versions pinned byCargo.lock. - Next steps: fill gaps with The Book, build practice volume with rustlings, and ship one small CLI tool. That concludes the basics course.