Skip to content
ExponaBLOG
RUST PROGRAMMING · DAY 076 / 100

Common Rust compiler errors: what developers should know

Errors beginners see.

Expona Admin7 min read

Introduction

If you've been working with Rust for a few months, you've likely encountered compiler errors that felt cryptic or overly strict compared to languages you've used before. Common Rust compiler errors: what developers should know is essential knowledge because the Rust compiler isn't trying to frustrate you—it's enforcing memory safety guarantees at compile time rather than letting bugs surface in production. Understanding the four most frequent error categories will accelerate your development cycle and help you write safer, more idiomatic code.

We've built production systems in Rust across e-commerce and SaaS platforms, and we've seen these same errors repeatedly across teams. Rather than treating them as obstacles, we've learned to view them as the compiler helping us catch logic errors early. Let's walk through each one with real scenarios you'll encounter.

Borrow of moved value

The "borrow of moved value" error is the first wall most developers hit. In Rust, ownership is strict: when you move a value to a function or into another binding, the original variable can no longer be used. This prevents use-after-free bugs but requires intentional thinking about data flow.

Consider a common pattern: loading data from a database and then trying to use it twice.

rust
fn process_user_data(user_id: u64) {
    let user = fetch_user_from_db(user_id);
    
    log_user_activity(&user);  // borrows user
    notify_user(user);           // moves user
    
    // ERROR: borrow of moved value: `user`
    print_user_info(&user);     // can't use user anymore
}

The compiler tells you that notify_user takes ownership of user, so when you try to reference it in print_user_info, the value is already gone. The fix depends on what you need:

rust
fn process_user_data(user_id: u64) {
    let user = fetch_user_from_db(user_id);
    
    log_user_activity(&user);       // borrows
    print_user_info(&user);         // borrows
    notify_user(user);               // moves (last use)

The key insight: arrange your code so immutable borrows happen first, and moves happen last. If a function truly needs ownership because it's storing the value or transforming it fundamentally, make that explicit in your function signature and ordering.

In production systems, this often surfaces when you're building request pipelines. You load a request, validate it, transform it, and return it. By borrowing for validation and only moving once you're done, you avoid this error entirely.

Cannot borrow as mutable more than once

Rust's second major borrow rule: you can have many immutable borrows or one mutable borrow, never both at the same time. This prevents data races and aliasing bugs. When you violate it, the compiler stops you.

This error commonly appears in code that modifies state while iterating or when passing the same data to multiple functions that each want to mutate it.

rust
fn update_user_settings(user: &mut User, settings: Vec<Setting>) {
    for setting in &settings {
        user.apply_setting(setting);      // first mutable borrow
    }
    
    user.validate();                       // second mutable borrow
    user.save_to_db();                     // ERROR: cannot borrow `user` as mutable more than once
}

Actually, the above code compiles because borrows end after each method call. Let's see a case that doesn't:

rust
fn process_order(order: &mut Order) {
    let total = &mut order.total;
    let items = &mut order.items;
    
    for item in items {
        *total += item.price;  // ERROR: cannot borrow `order` as mutable more than once
    }
}

Here we've taken two mutable references from the same struct simultaneously. The compiler prevents this because if one reference modified the underlying data, the other would see inconsistent state.

The solution depends on context:

rust
fn process_order(order: &mut Order) {
    // Option 1: Take mutable refs one at a time
    let total = &mut order.total;
    *total = 0;
    
    let items = &mut order.items;
    for item in items {
        *total += item.price;
    }
    
    // Option 2: Use interior mutability for selective mutation
    // Option 3: Refactor to work on separate fields independently
}

In our e-commerce systems, this error taught us to design data structures with borrowing in mind. Rather than holding a reference to the entire order while modifying multiple fields, we either modify one field at a time or restructure to separate mutable and immutable access paths.

Lifetime may not live long enough

Lifetimes are Rust's way of tracking how long references are valid. When you return a reference from a function or store it in a struct, the compiler needs to ensure the reference won't outlive the data it points to. Violating this causes the "lifetime may not live long enough" error.

This is abstract until you hit it in code:

rust
fn get_user_email(user_id: u64) -> &String {
    let user = fetch_user(user_id);  // local variable
    &user.email                      // ERROR: cannot return reference to local variable
}

user is created inside the function and destroyed when the function returns. Returning a reference to its field is invalid because the caller would hold a reference to freed memory.

The fix: return owned data, not a reference:

rust
fn get_user_email(user_id: u64) -> String {
    let user = fetch_user(user_id);
    user.email  // ownership transferred to caller
}

Lifetimes become more complex with structs that hold references:

rust
struct UserCache<'a> {
    name: &'a str,
}

fn create_cache() -> UserCache {
    let name = String::from("Alice");
    UserCache {
        name: &name,  // ERROR: `name` does not live long enough
    }
}

The struct's lifetime parameter 'a says "this struct holds a reference valid for lifetime 'a". But name is local and dropped at function end, so it can't satisfy that contract.

Solution: own the data:

rust
struct UserCache {
    name: String,
}

fn create_cache() -> UserCache {
    UserCache {
        name: String::from("Alice"),
    }
}

In production, lifetime errors often appear when designing APIs. A caching layer, for instance, might store request data. If you try to store references to the original request, the lifetime won't work—you need to own the data or use reference counting structures like Arc and Rc.

Mismatched types

Type mismatches are straightforward in concept but can be confusing in practice, especially when generics, trait objects, or inference are involved. The compiler reports the expected type and the type you provided.

Simple case:

rust
fn process_payment(amount: i32) {
    let user_input: f64 = 19.99;
    process_payment(user_input);  // ERROR: mismatched types
}

A more subtle case involves trait objects and function pointers:

rust
trait Handler {
    fn handle(&self, req: Request);
}

fn register_handler(h: Box<dyn Handler>) {
    // ...
}

fn my_handler(req: Request) {
    println!("Handling: {:?}", req);
}

register_handler(Box::new(my_handler));  // ERROR: mismatched types

my_handler is a function, not a struct implementing Handler. To use it, you'd need to wrap it:

rust
struct FnHandler<F>(F);

impl<F> Handler for FnHandler<F>
where
    F: Fn(Request),
{
    fn handle(&self, req: Request) {
        (self.0)(req);
    }
}

register_handler(Box::new(FnHandler(my_handler)));

Or use a closure:

rust
register_handler(Box::new(|req| {
    println!("Handling: {:?}", req);
}));

Generic type mismatches are common too. In a CRM system processing batches of records:

rust
fn batch_process<T>(items: Vec<T>) {
    let results: Vec<String> = items.iter().map(|item| item.to_string()).collect();
}

If T doesn't implement ToString, the compiler rejects it. You'd add a trait bound:

rust
fn batch_process<T: ToString>(items: Vec<T>) {
    let results: Vec<String> = items.iter().map(|item| item.to_string()).collect();
}

Common mistakes

  • Assuming moves are optional: In Rust, if a function takes ownership, it's intentional. Read the signature carefully. If you need to keep using the value, pass a reference.
  • Treating mutable borrows like immutable ones: You can't take a mutable reference and an immutable reference to the same data simultaneously. Organize code to avoid this, or use interior mutability patterns.
  • Returning references without lifetimes: Always think about whether you're returning borrowed data or owned data. If borrowed, the caller must keep the source alive.
  • Ignoring compiler hints: Error messages include the actual and expected types. Read them fully before searching online. Often, the fix is in the message.
  • Over-using dynamic dispatch: Trait objects and dyn are powerful but add complexity. Static dispatch with generics is often clearer and faster.

Key takeaways

  • Moved values can't be used again; order code to move last or return owned data instead of references.
  • Multiple mutable borrows to the same data are forbidden; restructure to borrow one field at a time or use interior mutability when appropriate.
  • References must outlive the data they point to; own data in structs or use reference counting for shared ownership across lifetimes.
  • Type mismatches require trait bounds or explicit conversions; trust the compiler's type inference but provide bounds when generics are involved.
  • These errors are features, not bugs—they catch real safety issues before production and guide you toward idiomatic Rust patterns.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles