Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error Messages, Decoded

This chapter is the practice. Real compiler errors, line by line, decoded into what they mean and what to change. The errors are quoted as the compiler emits them (or close enough — output formatting drifts slightly between rustc versions). The skill we’re building is the ability to look at a paragraph of error and find the actual fix.

Error 1: cannot infer an appropriate lifetime

error: lifetime may not live long enough
   --> src/lib.rs:14:9
    |
12  |     pub fn longest(&self, other: &str) -> &str {
    |                    -----         ----     - let's call the lifetime of this reference `'1`
    |                    |             |
    |                    |             let's call the lifetime of this reference `'2`
    |                    let's call the lifetime of this reference `'3`
13  |         if self.0.len() > other.len() {
14  |             other
    |             ^^^^^ associated function was supposed to return data with lifetime `'3` but it is returning data with lifetime `'2`

What the compiler is telling you. The function returns &str. Lifetime elision applied rule 3: there’s a &self, so the elided return lifetime is 'self. The function tried to return other, which has a different lifetime. The lifetimes '2 and '3 are not the same, so the return value’s actual lifetime ('2, the lifetime of other) doesn’t match the declared lifetime ('3, the lifetime of self).

Why it’s telling you that. Elision picked the wrong default. It assumed &self-bound output, but the function actually wants to return whichever of the two arguments is longer — meaning the return must be tied to the common lifetime of both, which has to be explicit.

The fix.

#![allow(unused)]
fn main() {
pub fn longest<'a>(&'a self, other: &'a str) -> &'a str {
    if self.0.len() > other.len() { &self.0 } else { other }
}
}

Now both inputs and the output share 'a, and the compiler can pick 'a as the intersection.

The general rule. When elision picks a &self-bound return for a function that should return one of multiple inputs, you have to write the lifetimes explicitly. This pattern accounts for a large fraction of “simple” lifetime errors.

Error 2: borrowed value does not live long enough

error[E0597]: `local` does not live long enough
   --> src/main.rs:6:18
    |
5   |     let local = String::from("hello");
    |         ----- binding `local` declared here
6   |     let r: &'static str = &local;
    |            ----------    ^^^^^^ borrowed value does not live long enough
    |            |
    |            type annotation requires that `local` is borrowed for `'static`
7   | }
    | - `local` dropped here while still borrowed

What the compiler is telling you. You annotated a reference as &'static str. The reference points at local, a String that goes out of scope at the end of the function. The compiler is being asked to enforce that the reference outlives 'static, which is not satisfiable for a stack-local value.

Why it’s telling you that. 'static is not “lives forever” but “is not borrowed from anything that ends.” A stack-allocated String ends at scope exit. Therefore a reference into it cannot satisfy 'static.

The fix, depending on intent.

If you wanted a &str borrow with the local’s lifetime, drop the 'static:

#![allow(unused)]
fn main() {
let r: &str = &local;
}

If you wanted 'static because something else demanded it (e.g., tokio::spawn), the right fix is usually to own the data instead of borrowing:

#![allow(unused)]
fn main() {
let r: String = local;  // move
// or, if you need to keep `local` separately:
let r: String = local.clone();
}

The general rule. 'static bounds usually mean “you must own this or it must come from a literal.” If you’re trying to satisfy a 'static bound with a borrow of local data, you have a structural problem, not a typo problem. Convert to ownership.

Error 3: implementation of FnOnce is not general enough

error: implementation of `FnOnce` is not general enough
   --> src/main.rs:14:5
    |
14  |     accept_callback(parse_first);
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `FnOnce` is not general enough
    |
    = note: `fn(&'2 [u8]) -> &'2 [u8] {parse_first}` must implement `FnOnce<(&'1 [u8],)>`, for any lifetime `'1`...
    = note: ...but it actually implements `FnOnce<(&'2 [u8],)>`, for some specific lifetime `'2`

What the compiler is telling you. accept_callback requires its argument to be a closure that works for any lifetime (for<'1>). You passed a function parse_first that works for some specific lifetime ('2). The two are incompatible: “works for any” is stronger than “works for one specific.”

Why it’s telling you that. This is the canonical HRTB inference failure. Either the function parse_first is genuinely not higher-ranked (its definition picks a particular lifetime), or the inference engine can’t see that it is.

The fix. Almost always, parse_first is fine, and the issue is that its signature was inferred too restrictively. Make the higher-ranking explicit:

#![allow(unused)]
fn main() {
fn parse_first<'a>(bytes: &'a [u8]) -> &'a [u8] {
    bytes.split(|&b| b == 0).next().unwrap_or(&[])
}
}

If it already looks like that, the problem may be on the consumer side — accept_callback’s bound is not asking for for<'a> cleanly. Check chapter 3, particularly the “patterns that work” section.

The general rule. “Not general enough” means the supplied function works for one lifetime but the trait bound asks for all of them. The answer is to make the function generic over the lifetime if it isn’t, or to relax the trait bound if you can.

Error 4: future cannot be sent between threads safely

error: future cannot be sent between threads safely
   --> src/main.rs:18:18
    |
18  |     tokio::spawn(do_work(&shared));
    |                  ^^^^^^^^^^^^^^^^ future returned by `do_work` is not `Send`
    |
note: future is not `Send` as this value is used across an await
   --> src/main.rs:9:23
    |
8   |     let cell = Rc::new(RefCell::new(0));
    |         ---- has type `Rc<RefCell<i32>>` which is not `Send`
9   |     other_async_op().await;
    |                       ^^^^^ await occurs here, with `cell` maybe used later

What the compiler is telling you. The future returned by do_work contains an Rc<RefCell<i32>> field across the await on line 9. Rc is not Send (it has a non-atomic refcount). Therefore the future is not Send. tokio::spawn requires Send.

Why it’s telling you that. Auto-traits propagate structurally. A struct containing a non-Send field is non-Send. The state machine is a struct. Rc is one of its fields. Game over.

The fix. Replace Rc with Arc. (And RefCell with Mutex if you also need to mutate it across threads.)

#![allow(unused)]
fn main() {
let cell = Arc::new(Mutex::new(0));
}

If you can’t replace it (because some other constraint forces Rc), the alternative is to not spawn the future on a multi-threaded executor. Use tokio::task::LocalSet and spawn_local, which do not require Send.

The general rule. Send errors on futures point at one offending value. Replace the value with a Send equivalent (Arc for Rc, tokio::sync::Mutex for RefCell-when-held-across-await, atomic types for Cells). If multiple values are offending, the error will only show one at a time; expect to fix several in sequence.

Error 5: the parameter type 'T' may not live long enough

error[E0310]: the parameter type `T` may not live long enough
  --> src/lib.rs:5:5
   |
5  |     Box::new(value)
   |     ^^^^^^^^^^^^^^^
   |     |
   |     the parameter type `T` must be valid for the static lifetime...
   |     ...so that the type `T` will meet its required lifetime bounds
   |
help: consider adding an explicit lifetime bound...
   |
3  |     pub fn into_box<T: 'static>(value: T) -> Box<dyn Display> {
   |                      +++++++++

What the compiler is telling you. Box<dyn Display> is sugar for Box<dyn Display + 'static>. Trait objects have an implicit 'static bound when no other lifetime is given. To put T in a Box<dyn Display>, T must satisfy 'static. Currently T has no such bound.

Why it’s telling you that. Because the compiler doesn’t know whether T is String (which is 'static) or &'a str (which isn’t). It refuses to assume.

The fix. Add the bound:

#![allow(unused)]
fn main() {
pub fn into_box<T: Display + 'static>(value: T) -> Box<dyn Display> {
    Box::new(value)
}
}

If you genuinely want to support non-'static types, change the trait object’s lifetime:

#![allow(unused)]
fn main() {
pub fn into_box<'a, T: Display + 'a>(value: T) -> Box<dyn Display + 'a> {
    Box::new(value)
}
}

The general rule. Trait objects default to 'static bound. If you’re putting a generic into a trait object, either constrain the generic to 'static or thread a lifetime through both.

Error 6: cannot borrow as mutable more than once

error[E0499]: cannot borrow `*v` as mutable more than once at a time
  --> src/main.rs:6:5
   |
4  |     let first = &mut v[0];
   |                       - first mutable borrow occurs here
5  |     let second = &mut v[1];
   |                       ^ second mutable borrow occurs here
6  |     println!("{}, {}", first, second);
   |                        ----- first borrow later used here

What the compiler is telling you. Both &mut v[0] and &mut v[1] are calls to IndexMut::index_mut, which takes &mut self and returns &mut Output. The compiler can only see that you’re calling index_mut twice on the same Vec; it doesn’t have type-level information that the two indices are distinct.

Why it’s telling you that. The borrow checker reasons about types, not values. IndexMut is just a method. The compiler cannot know at the type level that index 0 and index 1 don’t alias.

The fix. Use a method that returns multiple disjoint mutable references. The standard library provides split_at_mut:

#![allow(unused)]
fn main() {
let (left, right) = v.split_at_mut(1);
let first = &mut left[0];
let second = &mut right[0];
}

Or, on Rust 1.77+, [T]::get_disjoint_mut (formerly get_many_mut):

#![allow(unused)]
fn main() {
let [first, second] = v.get_disjoint_mut([0, 1]).unwrap();
}

These functions have signatures like fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]), which return two mutable references with the type-system-visible guarantee that they don’t overlap.

The general rule. When you need multiple mutable borrows into a collection, use the collection’s “split” methods. The borrow checker can’t infer disjointness from indices; the API has to express it in the return type.

Error 7: expected fn pointer, found fn item

error[E0308]: mismatched types
  --> src/main.rs:5:9
   |
5  |     funcs.push(double);
   |                ^^^^^^ expected fn pointer, found fn item
   |
   = note: expected fn pointer `fn(i32) -> i32`
              found fn item `fn(i32) -> i32 {double}`
help: consider casting to a fn pointer
   |
5  |     funcs.push(double as fn(i32) -> i32);
   |                       +++++++++++++++++

What the compiler is telling you. double is a fn item, which is a zero-sized type unique to the function double. funcs is a Vec<fn(i32) -> i32>, which holds fn pointers — runtime function pointers, all the same type. Fn items coerce to fn pointers, but the coercion has to happen at a point where the target type is known.

Why it’s telling you that. Each function in Rust has its own zero-sized type (an artifact of monomorphization). When you put a function in a Vec<fn(...)>, the elements are runtime pointers, so the fn item must be coerced to a pointer first.

The fix. Cast or annotate:

#![allow(unused)]
fn main() {
funcs.push(double as fn(i32) -> i32);
// or:
let f: fn(i32) -> i32 = double;
funcs.push(f);
}

Or, if you want the heterogeneous-functions case, use Box<dyn Fn(...)> instead of fn pointers, which works for closures too.

The general rule. Fn items and fn pointers are different. Coercion is implicit in many contexts but not in Vec::push (because push doesn’t have a known target type for inference to use). When you see this error, force the coercion or use a dyn Fn type.

Error 8: the trait Sized is not implemented

error[E0277]: the size for values of type `dyn Display` cannot be known at compilation time
  --> src/main.rs:3:9
   |
3  |     let x: dyn Display = 5;
   |         ^ doesn't have a size known at compile-time

What the compiler is telling you. dyn Display is an unsized type. You cannot have a stack variable of unsized type. You need to put it behind a pointer.

Why it’s telling you that. Trait objects have unknown size at compile time (different implementors are different sizes). Stack allocation requires a known size.

The fix. Box it:

#![allow(unused)]
fn main() {
let x: Box<dyn Display> = Box::new(5);
}

Or take a reference:

#![allow(unused)]
fn main() {
let n = 5;
let x: &dyn Display = &n;
}

The general rule. dyn Trait is unsized. Always behind a pointer (Box, &, Rc, Arc, Pin<Box>). Never bare.

Error 9: borrowed data escapes outside of method

error[E0521]: borrowed data escapes outside of method
   --> src/lib.rs:7:9
    |
4   |   pub fn store(&mut self, item: &str) {
    |                ---------  ----
    |                |          |
    |                |          `item` is a reference that is only valid in the method body
    |                let's call the lifetime of this reference `'1`
...
7   |         self.items.push(item);
    |         ^^^^^^^^^^^^^^^^^^^^^
    |         |
    |         `item` escapes the method body here
    |         argument requires that `'1` must outlive `'_`

What the compiler is telling you. self.items has some lifetime — maybe Vec<&'a str> for some 'a. item has a different (probably shorter) lifetime, called '1 here. Pushing item into self.items would extend item’s lifetime to whatever self.items requires, which the compiler can’t prove.

Why it’s telling you that. Containers of references have a fixed lifetime parameter. You can’t insert a reference with a shorter lifetime than the container’s parameter without somehow narrowing the container’s lifetime, which &mut self won’t allow (because of variance — see chapter 2).

The fix. Either store owned data:

#![allow(unused)]
fn main() {
pub fn store(&mut self, item: String) { self.items.push(item); }
}

Or take the borrow with the right lifetime:

#![allow(unused)]
fn main() {
pub fn store<'a>(&mut self, item: &'a str) where Self: 'a, ... 
// quickly gets complicated; usually owned is simpler
}

In practice, store owned data. If your container has lifetime parameters, every method gets harder; the cure is usually worse than the disease.

The general rule. Containers of references are a tax. You pay the tax forever. Default to containers of owned data.

Error 10: recursion in an async fn requires boxing

error[E0733]: recursion in an `async fn` requires boxing
  --> src/lib.rs:1:1
   |
1  | async fn factorial(n: u64) -> u64 {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2  |     if n <= 1 { 1 } else { n * factorial(n - 1).await }
   |                                ----------------------- recursive call here
   |
   = note: a recursive `async fn` call must introduce indirection such as `Box::pin` to avoid an infinitely sized future

What the compiler is telling you. Async functions desugar to state machines whose size includes the size of any inner futures stored across awaits. Recursive async functions would have infinite size: the state machine for factorial(n) contains a state machine for factorial(n-1), which contains one for factorial(n-2), on down. The compiler can’t compute the size.

Why it’s telling you that. This is the unavoidable consequence of stackless coroutines (chapter 4). The state of the call stack is encoded in the type. Recursion turns the type infinite.

The fix. Box the recursive call:

#![allow(unused)]
fn main() {
async fn factorial(n: u64) -> u64 {
    if n <= 1 { 1 } else { n * Box::pin(factorial(n - 1)).await }
}
}

Box::pin heap-allocates the inner future, so its contribution to the outer state machine is just a pointer (fixed size). The recursion is now flat in the type system.

The general rule. Recursive async needs Box::pin. There is no way around it short of rewriting the function iteratively. The cost is one heap allocation per recursive level. For most use cases this is fine; for performance-critical recursion, prefer iteration.

What this practice gives you

Reading these errors is the unavoidable skill. The compiler is telling you the truth — the words mean what they say — but the truth is filtered through a vocabulary that takes time to acquire. Once you’ve read enough of these, the same shape becomes recognizable in five seconds: “oh, that’s the elision-picked-the-wrong-default thing” or “oh, that’s an HRTB inference failure” or “oh, that’s Send propagating through a RefCell field.”

The compiler is the same compiler. The errors are produced by the same algorithms. The patterns repeat. The pattern-matching is the skill.