Rust Respects Your Time, Dude
My first hour of Rust ended with a code snippet I could not compile. I had read the ownership chapter once and I still could not convince the compiler that I was allowed to touch my own data. The thing that eventually got me through it was the error message itself, which had already told me - in the first run, before I started guessing - which borrow was still alive and where it got used later.
That is the part people leave out of the learning-curve conversation. The curve is real. Ownership, lifetimes, and the trait system are genuinely harder than what most languages ask of you on day one. What nobody told me is that the bill arrives up front and the language spends the rest of your time giving it back: rustc doesn't just reject your code, it diagnoses it and hands you the patch. Most of the time the patch is correct as written. You pay in weeks and you get repaid in the thousands of twenty-minute holes you never fall into.
I care about this more now than I did two years ago, because I'm not the only one reading those messages anymore. A model is. And a compiler that explains itself precisely is a validator an agent can loop against without me in the room.
What a bad error message actually costs#
I write Scala too, and it's a language I like. What drew me in was the functional approach - it lined up neatly with the applied mathematics I studied at university, so the notation felt like something I already knew. It's also, unfortunately, where I learned what a vague diagnostic costs you, because the cost is not the ten seconds of confusion. It's that the message gives you nothing to act on, so you go back to bisecting your own code by hand.
One example is a type mismatch where the two types are printed identically. The
Scala bug tracker has carried
one of these since 2012 - found and
required render as the same string, and the compiler has just told you those two
identical things don't match.
When a message like that lands, I want to yell at the terminal: this job is hard enough without you making it harder. The message isn't wrong, though. It's accurate and unusable, which is the worst combination, because you can't even file a bug against it. You just quietly lose twenty minutes.
What rustc says instead#
Here's the shape of a Rust diagnostic. This is the borrow-checker error everyone meets in their first week:
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &v[0];
| - immutable borrow occurs here
4 | v.push(4);
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("{first}");
| ------- immutable borrow later used here
Read what's in there. A stable machine-readable code (E0502, and
rustc --explain E0502 prints a page on it). The exact span that failed. And -
this is the part that does the work - the two other spans that explain the
failure: where the conflicting borrow started, and where it's used later. That last
line is the whole reason the error exists: it shows you the lifetime that makes the
rule bite, instead of announcing that a rule was broken.
Compare the information content with found X, required X. One tells you a fact
about your program. The other tells you the compiler is unhappy.
The suggestion is usually the fix#
Then there's the part that surprised me. Rust doesn't stop at the diagnosis:
error[E0308]: mismatched types
--> src/main.rs:2:21
|
2 | let s: String = "hello";
| ------ ^^^^^^^ expected `String`, found `&str`
| |
| expected due to this
|
help: try using a conversion method
|
2 | let s: String = "hello".to_string();
| ++++++++++++
That help block is a rendered diff with the insertion marked, and most of the
time you can apply it verbatim and move on. Add a
&, add .clone(), add the missing trait bound, add the use line for the trait
whose method you called. The compiler worked out the fix while it was working out
the complaint, and it decided to show you both.
That's also why the tooling around it works as well as it does. cargo fix applies
the machine-applicable suggestions across the whole crate. cargo clippy --fix
does the same for the lint layer, where the suggestions get opinionated - collapse
this match into an if let, this unwrap_or_else(|| ...) wants unwrap_or_default.
The suggestions are structured data the compiler emits; --error-format=json gives
you every diagnostic with its spans and its suggested_replacement string, and the
tools are just consumers of it.
Two things fall out of that. The obvious one: my dumbest mistakes cost me a keystroke instead of a lookup. The one I didn't see coming: a diagnostic that carries a machine-applicable patch is a diagnostic a program can act on. And I have a program that would like to act on it.
A gate the model can run#
In Bounded Chaos I described the deterministic gates my
workflow runs before a task is allowed to be done - lint, format, type-check,
tests. The point of a gate is that it doesn't have an opinion and can't be argued
with. The model either passes it or doesn't.
A statically typed, compiled language hands you one of those gates for free, and it
is a much bigger gate than it looks. Every function signature is an assertion the
model wrote down and now has to satisfy. Every enum it matches on is exhaustively
checked. Every borrow it took is verified against every other borrow in scope. In a
dynamic language, the equivalent claims are checked at runtime by tests the model
also wrote - which means the model is grading its own homework twice. cargo check
is the one participant in the loop with no incentive to agree with anything.
The quality of the messages is what turns that gate from a pass/fail into a
teacher. A model handed error: type error at line 40 gets one bit of information
and starts guessing, and a guessing loop is how you burn forty thousand tokens
producing something plausible and wrong. A model handed the E0502 above gets the
rule, the two conflicting spans, and the reason the rule applies here, which is
enough to edit the actual defect. Where a suggestion is attached, the loop
degenerates to applying it, and the iteration ends on the next pass.
Where this is going#
I'm not claiming Rust is without similar flaws, and I'm aware "respects your time"
is a strange thing to say about a language you can wait ninety seconds to build in.
Compile times are what they are, the trait-heavy async ecosystem will still hand
you a page-long error about a future that isn't Send, and there are days the
borrow checker is right and I still resent it. And rustc has its own
accurate-and-unusable
moments. The difference is that
these are the edges rather than the default, and each one is a filed bug rather than
something a library author was supposed to opt out of. The learning curve is a real
cost you pay in real weeks. The trade is that the waiting is scheduled and the
debugging isn't: I know what a cold build costs me, and I never know what an
afternoon of found X, required X costs until it's gone.
My point is also that the thing which makes those fixes survivable turns out to be the same property that makes the language good to hand an agent.
So the criterion I now apply when picking a language for work I intend to hand off: not how fast I can write it, but how much its compiler explains to whoever gets it wrong. Every language spends your time. This one tells you where it went.