Every rustc error has two spans, and they mean different things

# rust# beginners# compilers# learning
Every rustc error has two spans, and they mean different thingsMykhailo

The line rustc points at is usually not the line you edit. The message is doing more work than most...

The line rustc points at is usually not the line you edit. The message is doing more work than most people read.

Here's the smallest example, E0384:

fn main() {
    let x = 5;
    x = 6;
    println!("{x}");
}
Enter fullscreen mode Exit fullscreen mode
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:3:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut x = 5;
  |         +++
Enter fullscreen mode Exit fullscreen mode

Two spans. Line 3 carries the carets, because that's where the rule broke. Line 2 carries a single dash, because that's where you made the decision that broke it. The header names the failure, the dash names the cause, and the help: block edits the cause, not the failure. Read in that order the error stops being a complaint and turns into a two line story: you decided here, it went wrong there.

Most people stop at the header, add mut, and keep going. That works for E0384 and it teaches you nothing that survives to the next error.

The same shape, one step harder

E0382, borrow of moved value:

fn main() {
    let name = String::from("crab");
    let other = name;
    println!("{name}");
}
Enter fullscreen mode Exit fullscreen mode
error[E0382]: borrow of moved value: `name`
 --> src/main.rs:4:16
  |
2 |     let name = String::from("crab");
  |         ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
3 |     let other = name;
  |                 ---- value moved here
4 |     println!("{name}");
  |                ^^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let other = name.clone();
  |                     ++++++++
Enter fullscreen mode Exit fullscreen mode

Carets at line 4. But nothing on line 4 is wrong in isolation, printing a String is fine. The dashes carry the actual content: line 2 says why the type behaves this way, line 3 says where the ownership left. The fix lands on line 3, one line above the report and two below the explanation.

That middle dash on line 2 is the whole ownership model in one sentence, and it's the part people skim because it's long and doesn't have carets under it.

And once more, with three

E0502:

error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let first = &numbers[0];
  |                  ------- immutable borrow occurs here
4 |     numbers.push(4);
  |     ^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{first}");
  |                ----- immutable borrow later used here
Enter fullscreen mode Exit fullscreen mode

Here the second span split in two, one before the carets and one after. That's the borrow checker showing you a span of time, not a span of text: the borrow starts at line 3, is still alive at line 5, and line 4 sits inside it. Delete the println! and the error goes away, which is a strange thing to be true about code three lines up from the complaint.

So the rule generalises a little past "two spans". Carets are where the compiler stopped. Dashes are the evidence, and there can be one or several. The fix goes wherever the evidence points.

Why I think this matters more than the error codes

I've been writing one page per rustc error code and I'm about fifty in, out of roughly six hundred codes that exist. Somewhere around the twentieth it stopped feeling like fifty separate things to memorise. The codes differ, the span layout doesn't, and the layout is the part you can actually carry into an error you've never seen. A beginner who reads dashes before carets can handle E0499 the first time they meet it. A beginner who's memorised twelve codes still can't.

My unresolved doubt, and it's a real one: rustc --explain E0384 is already built into the toolchain, offline, with no site to visit. It explains the code in general. It does not explain your diagnostic, with your line numbers and your dashes in it, which is the thing I think people need. I'm not certain that gap is big enough to justify a page per code. I've got about fifty of them here if you want to tell me either way: https://codecrab.app/?r=devto-two-spans

What's the error you kept fixing at the wrong line before you noticed the dashes?