Free Handbook · Every example compiled & verified

Errors & Debugging

How to read rustc diagnostics and panic backtraces, the 15 errors every Rust beginner hits with real messages and fixes, plus dbg!, clippy and rust-analyzer.

0 / 140 lessons🔥 0 day streak
ShareXLinkedIn

Module 11 · what you'll be able to do

  • Read a rustc diagnostic part by part: error code, primary and secondary spans, note and help lines, and the suggested patch
  • Use rustc --explain to get the long explanation for any error code
  • Turn on RUST_BACKTRACE=1 and find the line in your own code that caused a panic
  • Recognise and fix the 15 errors Rust beginners hit most, from E0382 to integer overflow
  • Debug with dbg!, eprintln!, cargo check, clippy and rust-analyzer instead of guessing
01

Anatomy of a rustc error message

Rust has two kinds of failure, and they look nothing alike. A compile error comes from rustc before your program exists: no binary is produced and nothing runs. A panic happens while the program runs: it prints a message, unwinds the thread and exits with code 101. Most of your time as a beginner is spent on the first kind, and the good news is that rustc error messages are among the most helpful of any compiler. They are worth reading slowly, top to bottom.

Compile error

  • Starts with error[E0xxx]:
  • Points at a file, line and column with an arrow -->
  • No program is built, so nothing runs at all
  • Usually ends with a help: suggestion you can apply
  • Fix: change the code until cargo check is clean

Runtime panic

  • Starts with thread 'main' (id) panicked at
  • Gives the file:line:col where the panic fired
  • Code before that line already ran and printed
  • Set RUST_BACKTRACE=1 to see the call chain
  • Fix: handle the None/Err or the bad input the panic reveals

Here is a real diagnostic with every part labelled. The code borrows names[0], then pushes onto names while that borrow is still needed.

text
error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable
 --> main.rs:4:5
  |
3 |     let first = &names[0];
  |                  ----- immutable borrow occurs here
4 |     names.push(String::from("ben"));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{}", first);
  |                    ----- immutable borrow later used here

The full output ends with For more information about this error, try `rustc --explain E0502`.

  1. 1
    The headline

    error[E0502] is the error code; the sentence after it is the rule you broke. Search the code, not the sentence: codes never change wording.

  2. 2
    The location

    --> main.rs:4:5 is file, line, column of the primary problem. Editors make it clickable.

  3. 3
    The primary span

    The ^^^^ underline marks where the rule was actually broken (line 4, the push).

  4. 4
    Secondary spans

    The ----- underlines tell the story around it: where the conflicting borrow started (line 3) and where it is still used (line 5). Read them in line order to see the conflict unfold.

  5. 5
    note: and help:

    A note: adds context (often pointing into another function or the standard library). A help: is a suggestion, frequently with a patch where + marks added text and ~ a changed line. Treat it as a strong hint, not gospel: it fixes the symptom the compiler sees, which is not always your design problem.

Every error code has a long explanation with a broken example and a fixed one, available offline:

bash
$ rustc --explain E0499
A variable was borrowed as mutable more than once.

Erroneous code example:

```
let mut i = 0;
let mut x = &mut i;
let mut a = &mut i;
x;
// error: cannot borrow `i` as mutable more than once at a time
```

Please note that in Rust, you can either have many immutable references, or one
mutable reference. ...

The same text lives online in the Rust error codes index. When you see several errors at once, fix the first one and recompile: later errors are often knock-on effects.

Warnings are not errors, but read them
A warning: (unused variable, unused Result, unreachable code) still produces a binary. Many real bugs show up first as a warning, such as a Result you forgot to check. Aim for zero warnings; CI pipelines often enforce it with RUSTFLAGS="-D warnings".
02

Reading a panic and its backtrace

A panic prints three things: which thread panicked, where (file:line:col of the panic, which for unwrap is your .unwrap() call, not somewhere inside the standard library), and the message. That first line alone is often enough. When it is not, because the panicking function is called from many places, ask for the call chain with RUST_BACKTRACE=1.

rustmain.rs
fn parse_port(raw: &str) -> u16 {
    raw.parse().unwrap()
}

fn load_config(lines: &[&str]) -> u16 {
    parse_port(lines[1])
}

fn main() {
    let lines = ["host=db", "80a"];
    let port = load_config(&lines);
    println!("port {}", port);
}
bash
$ rustc -g main.rs
$ RUST_BACKTRACE=1 ./main
thread 'main' (13336186) panicked at main.rs:2:17:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::result::unwrap_failed
   3: <core::result::Result<u16, core::num::error::ParseIntError>>::unwrap
   4: main::parse_port
             at ./main.rs:2:17
   5: main::load_config
             at ./main.rs:6:5
   6: main::main
             at ./main.rs:11:16
   7: <fn() as core::ops::function::FnOnce<()>>::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Real output from a debug build (-g adds the line numbers; cargo run does the same and shows src/main.rs paths). The at lines pointing into the standard library's source are trimmed. On Windows PowerShell set the variable with $env:RUST_BACKTRACE=1 first.

  1. 1
    Start at the bottom

    The bottom frames are where the program began: runtime start-up, then main::main. Reading upward retraces the calls: main (line 11) called load_config (line 6), which called parse_port (line 2).

  2. 2
    Skip the machinery

    Frames 0 to 3 are the panic machinery and unwrap itself. They are the same for every unwrap panic; ignore them.

  3. 3
    Find the topmost frame in your code

    The highest frame named after your crate (main::parse_port, at main.rs:2:17) is where it blew up. The frame below it tells you who passed the bad value: load_config handed it lines[1], which is "80a".

  4. 4
    Fix the cause, not the line

    The bug is not really on line 2; it is that bad input can reach parse_port at all. Return a Result and let the caller report it (see the ? operator).

Debug vs release builds
cargo run builds in debug mode: file and line info in backtraces, and integer overflow checks turned on. cargo run --release optimises, so some frames are inlined away and overflow silently wraps instead of panicking. Reproduce bugs in debug first.
03

Ownership, borrowing and lifetime errors

These six are the borrow checker talking, and together they are most of what beginners fight. Each maps to a rule from Module 03 or Module 08. When you hit one, name the rule it enforces before you start changing code.

Error you will hit

1. E0382: borrow of moved value

rust
fn main() {
    let title = String::from("Rust");
    let mut tags = Vec::new();
    tags.push(title);
    println!("{} has {} tag", title, tags.len());
}
error[E0382]: borrow of moved value: `title`
 --> main.rs:5:31
  |
2 |     let title = String::from("Rust");
  |         ----- move occurs because `title` has type `String`, which does not implement the `Copy` trait
3 |     let mut tags = Vec::new();
4 |     tags.push(title);
  |               ----- value moved here
5 |     println!("{} has {} tag", title, tags.len());
  |                               ^^^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
4 |     tags.push(title.clone());
  |                    ++++++++
Why the compiler said that

push takes its argument by value, so ownership of the String moved into the vector. title is now an empty name.

The fix

Use the value through its new owner (tags[0]), print before moving, or clone if you truly need two copies.

rust
fn main() {
    let title = String::from("Rust");
    let mut tags = Vec::new();
    tags.push(title);
    println!("{} has {} tag", tags[0], tags.len());
}
Error you will hit

2. E0499: two mutable borrows at once

rust
fn main() {
    let mut scores = vec![10, 20, 30];
    let first = &mut scores[0];
    let last = &mut scores[2];
    *first += 1;
    *last += 1;
    println!("{:?}", scores);
}
error[E0499]: cannot borrow `scores` as mutable more than once at a time
 --> main.rs:4:21
  |
3 |     let first = &mut scores[0];
  |                      ------ first mutable borrow occurs here
4 |     let last = &mut scores[2];
  |                     ^^^^^^ second mutable borrow occurs here
5 |     *first += 1;
  |     ----------- first borrow later used here
  |
  = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
Why the compiler said that

Indexing borrows the whole vector, not just one element. The compiler does not reason about indexes, so two &mut scores[..] look like two writers to the same thing.

The fix

Finish with one borrow before starting the next, or split the slice as the help line says (split_at_mut, iter_mut) so each half is a separate borrow.

rust
fn main() {
    let mut scores = vec![10, 20, 30];
    let (left, right) = scores.split_at_mut(2);
    left[0] += 1;
    right[0] += 1;
    println!("{:?}", scores);
}
Error you will hit

3. E0502: mutable borrow while a shared borrow is alive

rust
fn main() {
    let mut names = vec![String::from("ana")];
    let first = &names[0];
    names.push(String::from("ben"));
    println!("{}", first);
}
error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable
 --> main.rs:4:5
  |
3 |     let first = &names[0];
  |                  ----- immutable borrow occurs here
4 |     names.push(String::from("ben"));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{}", first);
  |                    ----- immutable borrow later used here
Why the compiler said that

This one prevents a real memory bug. push may reallocate the vector's buffer, which would leave first pointing at freed memory.

The fix

Use the shared borrow before mutating, or clone the value out so no borrow is held across the push.

rust
fn main() {
    let mut names = vec![String::from("ana")];
    let first = names[0].clone();
    names.push(String::from("ben"));
    println!("{}", first);
}
Error you will hit

4. E0505: moving a value while it is borrowed

rust
fn consume(v: Vec<i32>) -> usize {
    v.len()
}

fn main() {
    let data = vec![1, 2, 3];
    let first = &data[0];
    let n = consume(data);
    println!("{} {}", first, n);
}
error[E0505]: cannot move out of `data` because it is borrowed
 --> main.rs:8:21
  |
6 |     let data = vec![1, 2, 3];
  |         ---- binding `data` declared here
7 |     let first = &data[0];
  |                  ---- borrow of `data` occurs here
8 |     let n = consume(data);
  |                     ^^^^ move out of `data` occurs here
9 |     println!("{} {}", first, n);
  |                       ----- borrow later used here
  |
help: consider cloning the value if the performance cost is acceptable
  |
7 |     let first = &data.clone()[0];
  |                      ++++++++
Why the compiler said that

consume takes ownership and frees the vector when it returns, but first still points into it.

The fix

Copy out what you need first (let first = data[0];, since i32 is Copy), or change consume to borrow (&[i32]) if it does not need ownership. The clone the help suggests works but is wasteful here.

rust
fn consume(v: &[i32]) -> usize {
    v.len()
}

fn main() {
    let data = vec![1, 2, 3];
    let first = &data[0];
    let n = consume(&data);
    println!("{} {}", first, n);
}
Error you will hit

5. E0597: borrowed value does not live long enough

rust
fn main() {
    let longest;
    {
        let word = String::from("temporary");
        longest = &word;
    }
    println!("{}", longest);
}
error[E0597]: `word` does not live long enough
 --> main.rs:5:19
  |
4 |         let word = String::from("temporary");
  |             ---- binding `word` declared here
5 |         longest = &word;
  |                   ^^^^^ borrowed value does not live long enough
6 |     }
  |     - `word` dropped here while still borrowed
7 |     println!("{}", longest);
  |                    ------- borrow later used here
Why the compiler said that

The reference would outlive its owner: word is freed at the inner closing brace, but longest is read afterwards. In C this compiles and reads garbage.

The fix

Make the owner live at least as long as the reference (declare it in the outer scope), or store an owned String instead of a reference.

rust
fn main() {
    let longest;
    {
        let word = String::from("temporary");
        longest = word;
    }
    println!("{}", longest);
}
Error you will hit

6. E0106: missing lifetime specifier

rust
fn longer(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    println!("{}", longer("hi", "hello"));
}
error[E0106]: missing lifetime specifier
 --> main.rs:1:32
  |
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`
help: consider introducing a named lifetime parameter
  |
1 | fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
  |          ++++     ++          ++          ++
Why the compiler said that

With two reference inputs, the elision rules cannot guess which one the returned reference comes from, so the caller would not know how long the result stays valid.

The fix

Name a lifetime that ties the output to the inputs, exactly as the help shows. Module 08 explains what 'a promises.

rust
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    println!("{}", longer("hi", "hello"));
}
04

Type, trait and name errors

The next five are about types and names. Rust never converts between types silently, and it only knows names you have declared or imported. The messages are short and the help: lines are usually exactly right.

Error you will hit

7. E0308: mismatched types (String where &str is expected)

rust
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

fn main() {
    let name = String::from("Ferris");
    println!("{}", greet(name));
}
error[E0308]: mismatched types
 --> main.rs:7:26
  |
7 |     println!("{}", greet(name));
  |                    ----- ^^^^ expected `&str`, found `String`
  |                    |
  |                    arguments to this function are incorrect
  |
note: function defined here
 --> main.rs:1:4
  |
1 | fn greet(name: &str) -> String {
  |    ^^^^^ ----------
help: consider borrowing here
  |
7 |     println!("{}", greet(&name));
  |                          +
Why the compiler said that

String (owned) and &str (borrowed) are different types. Deref coercion turns &String into &str, but only if you pass a reference in the first place.

The fix

Borrow with &name. Taking &str parameters is the right design: it accepts both literals and borrowed Strings.

rust
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

fn main() {
    let name = String::from("Ferris");
    println!("{}", greet(&name));
}
Error you will hit

8. E0277: trait bound not satisfied (u32 times f64)

rust
fn main() {
    let count: u32 = 3;
    let price: f64 = 2.5;
    let total = count * price;
    println!("{}", total);
}
error[E0277]: cannot multiply `u32` by `f64`
 --> main.rs:4:23
  |
4 |     let total = count * price;
  |                       ^ no implementation for `u32 * f64`
  |
  = help: the trait `Mul<f64>` is not implemented for `u32`
  = help: the following other types implement trait `Mul<Rhs>`:
            `&u32` implements `Mul<u32>`
            `&u32` implements `Mul`
            `u32` implements `Mul<&u32>`
            `u32` implements `Mul<Duration>`
            `u32` implements `Mul`
Why the compiler said that

E0277 means "this type does not implement a trait the code needs". Operators are traits (* is Mul), and there is no Mul<f64> for u32, because mixing integer and float arithmetic silently loses precision in other languages. The same code appears when you print a struct without Display or send an Rc to a thread.

The fix

Convert explicitly with as (or f64::from(count), which is lossless) so both sides have the same type.

rust
fn main() {
    let count: u32 = 3;
    let price: f64 = 2.5;
    let total = f64::from(count) * price;
    println!("{}", total);
}
Error you will hit

9. E0425: cannot find value in this scope

rust
fn main() {
    for i in 0..3 {
        let square = i * i;
    }
    println!("{}", square);
}
error[E0425]: cannot find value `square` in this scope
 --> main.rs:5:20
  |
5 |     println!("{}", square);
  |                    ^^^^^^
  |
help: the binding `square` is available in a different scope in the same function
 --> main.rs:3:13
  |
3 |         let square = i * i;
  |             ^^^^^^
Why the compiler said that

A let inside a block only exists until that block's closing brace. By line 5 square is gone. Typos (sqaure) produce the same code, with a "similar name exists" help.

The fix

Declare the variable in the scope where you use it, and assign to it inside the loop.

rust
fn main() {
    let mut square = 0;
    for i in 0..3 {
        square = i * i;
    }
    println!("{}", square);
}
Error you will hit

10. E0433: use of undeclared type (missing use)

rust
fn main() {
    let mut stock = HashMap::new();
    stock.insert("apple", 3);
    println!("{:?}", stock.get("apple"));
}
error[E0433]: cannot find type `HashMap` in this scope
 --> main.rs:2:21
  |
2 |     let mut stock = HashMap::new();
  |                     ^^^^^^^ use of undeclared type `HashMap`
  |
help: consider importing this struct
  |
1 + use std::collections::HashMap;
  |
Why the compiler said that

Only a small prelude (Vec, String, Option, Result, Box...) is imported automatically. Everything else must be brought in with use. For a crate you have not added to Cargo.toml the message is similar and the fix is cargo add.

The fix

Add the use line the help gives. rust-analyzer can insert it for you (the "import" quick fix).

rust
use std::collections::HashMap;

fn main() {
    let mut stock = HashMap::new();
    stock.insert("apple", 3);
    println!("{:?}", stock.get("apple"));
}
Error you will hit

11. E0599: no method found

rust
struct User {
    name: String,
}

fn main() {
    let u = User { name: String::from("ana") };
    println!("{}", u.greet());
}
error[E0599]: no method named `greet` found for struct `User` in the current scope
 --> main.rs:7:22
  |
1 | struct User {
  | ----------- method `greet` not found for this struct
...
7 |     println!("{}", u.greet());
  |                      ^^^^^ method not found in `User`
Why the compiler said that

The method does not exist on this type. Common causes: you never wrote the impl, the method belongs to a trait you have not imported with use (the help then names it), or you are calling it on the wrong type, such as Option<User> instead of User.

The fix

Define the method in an impl block, import the trait that provides it, or unwrap the Option/Result first.

rust
struct User {
    name: String,
}

impl User {
    fn greet(&self) -> String {
        format!("Hi, {}", self.name)
    }
}

fn main() {
    let u = User { name: String::from("ana") };
    println!("{}", u.greet());
}
05

Mutability and match errors

Two errors that come from Rust choosing the safe default: variables are immutable unless you say mut, and a match must handle every possible value.

Error you will hit

12. E0384: cannot assign twice to an immutable variable

rust
fn main() {
    let total = 0;
    for n in [4, 5, 6] {
        total += n;
    }
    println!("{}", total);
}
error[E0384]: cannot assign twice to immutable variable `total`
 --> main.rs:4:9
  |
2 |     let total = 0;
  |         ----- first assignment to `total`
3 |     for n in [4, 5, 6] {
4 |         total += n;
  |         ^^^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut total = 0;
  |         +++
Why the compiler said that

let bindings are immutable by default, and += is an assignment.

The fix

Add mut, or better, avoid the mutable accumulator: let total: i32 = [4, 5, 6].iter().sum();.

rust
fn main() {
    let mut total = 0;
    for n in [4, 5, 6] {
        total += n;
    }
    println!("{}", total);
}
Error you will hit

13. E0004: non-exhaustive patterns

rust
enum Status {
    Active,
    Suspended,
    Deleted,
}

fn label(s: Status) -> &'static str {
    match s {
        Status::Active => "active",
        Status::Suspended => "suspended",
    }
}

fn main() {
    println!("{}", label(Status::Deleted));
}
error[E0004]: non-exhaustive patterns: `Status::Deleted` not covered
  --> main.rs:8:11
   |
 8 |     match s {
   |           ^ pattern `Status::Deleted` not covered
   |
note: `Status` defined here
  --> main.rs:1:6
   |
 1 | enum Status {
   |      ^^^^^^
...
 4 |     Deleted,
   |     ------- not covered
   = note: the matched value is of type `Status`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
   |
10 ~         Status::Suspended => "suspended",
11 ~         Status::Deleted => todo!(),
   |
Why the compiler said that

A match is an expression that must produce a value for every input. This is a feature: add a variant to an enum and the compiler lists every match you need to update.

The fix

Add an arm for the missing variant. Prefer explicit arms over _ => for your own enums, so the next new variant is caught too.

rust
enum Status {
    Active,
    Suspended,
    Deleted,
}

fn label(s: Status) -> &'static str {
    match s {
        Status::Active => "active",
        Status::Suspended => "suspended",
        Status::Deleted => "deleted",
    }
}

fn main() {
    println!("{}", label(Status::Deleted));
}
06

Runtime panics: unwrap on None and integer overflow

These compile cleanly and fail when they run. Both are the program telling you it met a case you did not plan for.

Error you will hit

14. Runtime panic: called Option::unwrap() on a None value

rust
fn main() {
    let users = vec!["ana", "ben"];
    let found = users.iter().find(|u| u.starts_with('z'));
    println!("found {}", found.unwrap());
}
thread 'main' (13330311) panicked at main.rs:4:32:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Why the compiler said that

find returns None when nothing matches, and unwrap on None panics. The message carries no detail about which lookup failed, which is why production code prefers expect("reason") at minimum.

The fix

Handle the None with match, if let, unwrap_or or ?. Keep unwrap for cases that truly cannot fail, and for tests.

rust
fn main() {
    let users = vec!["ana", "ben"];
    match users.iter().find(|u| u.starts_with('z')) {
        Some(u) => println!("found {}", u),
        None => println!("no match"),
    }
}
Error you will hit

15. Runtime panic: attempt to subtract with overflow

rust
fn main() {
    let queue: Vec<u32> = Vec::new();
    let last_index = queue.len() - 1;
    println!("{}", last_index);
}
thread 'main' (13330905) panicked at main.rs:3:22:
attempt to subtract with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Why the compiler said that

len() returns usize, which cannot be negative. 0 - 1 underflows. Debug builds panic; release builds wrap to 18446744073709551615 and the bug turns into an out-of-bounds index somewhere else.

The fix

Ask the question you mean: queue.last() for the last element, checked_sub for arithmetic that may go below zero, or check is_empty() first.

rust
fn main() {
    let queue: Vec<u32> = Vec::new();
    match queue.len().checked_sub(1) {
        Some(i) => println!("{}", i),
        None => println!("queue is empty"),
    }
}
rustmain.rs
fn main() {
    let queue: Vec<u32> = Vec::new();
    let users = vec!["ana", "ben"];

    // Option-aware instead of unwrap: no panic possible
    match users.iter().find(|u| u.starts_with('z')) {
        Some(u) => println!("found {}", u),
        None => println!("no user starting with z"),
    }

    // checked arithmetic returns None instead of overflowing
    println!("{:?}", queue.len().checked_sub(1));
    println!("{}", queue.len().saturating_sub(1));
    println!("{:?}", queue.last());
    println!("{:?}", users.get(5));
}
Outputcompiled & run with real Rust
no user starting with z
None
0
None
None

The panic-free toolbox: checked_* returns an Option, saturating_* clamps at the limit, and .get(i) / .last() return Option where v[i] would panic with "index out of bounds".

Your turn

Replace users.get(5) with users[5], run it, and read the panic message and location.

07

Debugging tools: dbg!, cargo check, clippy, rust-analyzer

dbg!(expr) is the fastest debugging tool in Rust. It prints the file, line, the expression's source text and its value (with {:?}) to stderr, then returns the value, so you can wrap any expression in place without restructuring the code. Remove it before committing; clippy can flag leftovers.

rustmain.rs
fn discount(price: u32, percent: u32) -> u32 {
    let cut = dbg!(price * percent / 100);
    price - cut
}

fn main() {
    let total = dbg!(discount(250, 20)) + dbg!(discount(99, 10));
    println!("total = {}", total);
}
Outputcompiled & run with real Rust
total = 290

The output above is stdout. The terminal also shows these stderr lines from dbg!, in this order:
[main.rs:2:15] price * percent / 100 = 50
[main.rs:7:17] discount(250, 20) = 200
[main.rs:2:15] price * percent / 100 = 9
[main.rs:7:43] discount(99, 10) = 90
Notice 99 * 10 / 100 = 9: integer division truncates, and dbg! made that visible.

Your turn

Wrap price - cut in dbg! too. It still returns the value, so the function is unchanged.

ToolWhat it doesWhen to use it
dbg!(x)Prints file:line, expression and value to stderr; returns the valueQuick look at an intermediate value
eprintln!Like println! but to stderrProgress or diagnostic messages that must not mix with real output
cargo checkType-checks without generating code, much faster than a buildConstantly, while fixing compile errors
cargo clippyHundreds of extra lints: non-idiomatic code, likely bugs, slow patternsBefore every commit and in CI
cargo fmtFormats code to the standard styleOn save; ends style arguments in review
rust-analyzerLanguage server: inline errors, types on hover, go to definition, quick fixesAll the time, in VS Code, Zed, Neovim or a JetBrains IDE (RustRover has its own engine)
rust-lldb / rust-gdb, CodeLLDBA real debugger: breakpoints, stepping, inspecting variablesLogic bugs you cannot find by printing
cargo testRuns #[test] functions; panics become test failuresPin a bug down with a failing test, then fix it

Clippy is worth running from day one because it teaches idiomatic Rust. Here is part of its real output on a short beginner function that loops by index over a &Vec<String> and ends with return total;:

text
$ cargo clippy
warning: unneeded `return` statement
 --> src/main.rs:6:5
  |
6 |     return total;
  |     ^^^^^^^^^^^^
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#needless_return
  = note: `#[warn(clippy::needless_return)]` on by default
help: remove `return`
  |
6 -     return total;
6 +     total
  |

warning: writing `&Vec` instead of `&[_]` involves a new object where a slice will do
 --> src/main.rs:1:21
  |
1 | fn total_len(names: &Vec<String>) -> usize {
  |                     ^^^^^^^^^^^^
  |
help: change this to
  |
1 - fn total_len(names: &Vec<String>) -> usize {
1 + fn total_len(names: &[String]) -> usize {
  |

warning: the loop variable `i` is only used to index `names`
 --> src/main.rs:3:14
  |
3 |     for i in 0..names.len() {
  |              ^^^^^^^^^^^^^^

Each lint has a name (needless_return, ptr_arg, needless_range_loop) and a docs link. cargo clippy --fix applies the safe suggestions for you.

In real projects
Services log with the tracing or log crates rather than println!, so verbosity can be changed per module with an environment variable such as RUST_LOG=debug. CI usually runs cargo fmt --check, cargo clippy -- -D warnings and cargo test on every pull request; Module 12 sets that up with GitHub Actions.
08

Index of the 15 errors

Bookmark this table. Six of the fifteen are the borrow checker, and all six go away once you ask "who owns this, and who is still looking at it?"
#CodeMessage starts withUsual fix
1E0382borrow of moved valueUse the new owner, borrow instead of move, or clone
2E0499cannot borrow as mutable more than onceEnd the first &mut first, or split_at_mut
3E0502cannot borrow as mutable because it is also borrowed as immutableFinish reading before mutating, or clone the value out
4E0505cannot move out of ... because it is borrowedCopy out what you need, or make the callee borrow
5E0597does not live long enoughMove the owner to a longer-lived scope, or own the data
6E0106missing lifetime specifierAdd <'a> tying the output to an input
7E0308mismatched typesBorrow (&x), convert, or fix the return type
8E0277trait bound not satisfied / cannot multiplyConvert types, derive or implement the trait
9E0425cannot find value in this scopeFix the typo or declare it in the outer scope
10E0433use of undeclared typeAdd the use line (or cargo add the crate)
11E0599no method named ... foundWrite the impl, import the trait, or unwrap first
12E0384cannot assign twice to immutable variableAdd mut, or compute the value without mutation
13E0004non-exhaustive patternsAdd the missing match arms
14paniccalled `Option::unwrap()` on a `None` valuematch, if let, unwrap_or or ?
15panicattempt to subtract with overflowchecked_sub, saturating_sub, or .last()
Quick check

Your program panics and the backtrace lists 12 frames. Where do you look first?

Diagnostic
A compiler message: an error or warning with a code, a location and labelled spans.
Error code
An identifier like E0502. rustc --explain E0502 prints a long explanation with examples.
Span
The underlined piece of source. ^^^ marks the primary span (the problem), --- secondary spans (context).
Panic
A runtime failure that unwinds the current thread and, in main, exits with code 101.
Backtrace
The list of function calls active when a panic happened. Enabled with RUST_BACKTRACE=1.
dbg!
A macro that prints an expression with its location and value to stderr and returns the value.
Clippy
The official linter, run with cargo clippy. Catches non-idiomatic code and likely bugs.
rust-analyzer
The Rust language server that powers inline errors, hover types and quick fixes in editors.
JuniorHow do you approach a Rust compile error you have never seen before?

Read the first error only, top to bottom: the code and headline name the rule, the ^^^ span shows where it broke, and the --- spans show the other half of the conflict. Then read the help: line and decide whether its patch matches what I intended. If the message is unclear, rustc --explain E0xxx gives a worked example. I fix that one error and recompile with cargo check, because later errors are often caused by the first.

What they are really testing: Whether you read diagnostics methodically instead of changing code at random until it compiles.

Frequently asked questions

How do I see a full backtrace when a Rust program panics?
Set the environment variable RUST_BACKTRACE=1 before running (RUST_BACKTRACE=1 cargo run on macOS and Linux, $env:RUST_BACKTRACE=1 in PowerShell). Use RUST_BACKTRACE=full for every frame. Debug builds give file and line numbers for each frame.
What does rustc --explain do?
It prints the long-form documentation for an error code, such as rustc --explain E0382: what the rule is, a small program that triggers it, and how to fix it. It works offline and matches the online Rust error codes index.
Should I always follow the compiler's help suggestion?
Usually, but read it first. The suggestion fixes what the compiler sees, for example by adding .clone() or a lifetime. Sometimes the real fix is a design change, such as borrowing instead of taking ownership, which the compiler cannot know you wanted.

Finish the Rust handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.