7 min read

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.

RustEngineeringAI

Thanks for reading

More like it are on the way - stick around or say hi.

Get new posts by email