Ramblings of an aging IT geek
← Ramblings of an aging IT geek
rust

The Edition That Quietly Reshaped How I Write Rust

Looking back at the Rust 2018 edition and the handful of changes that genuinely altered the way I structure and reason about my code.

Code on a screen

I was tidying up an old crate the other day and noticed it still declared edition = "2018" in its Cargo.toml, which sent me down a small nostalgic rabbit hole. We have had a couple of editions since, and it is easy to forget how much the 2018 one changed about the day-to-day feel of writing Rust. The language did not get a dramatic new feature so much as a set of papercuts removed, and the cumulative effect on how I write was larger than any single headline item.

The module system is the obvious one. Pre-2018 Rust modules were a genuine source of confusion: extern crate declarations at the top of your lib, paths that behaved differently depending on where you stood, mod.rs files everywhere. The 2018 edition cleaned up paths so that they actually matched the mental model most people already had. You could drop the extern crate lines, you could put a module's contents in foo.rs next to a foo/ directory instead of burying it in foo/mod.rs, and crate-relative versus module-relative paths stopped being a guessing game.

A wide view of code on a monitor

The change I felt most, though, was ? working on Result and the maturing of error handling around it. Combined with the way the ecosystem settled, propagating errors with ? rather than a wall of match arms or the old try! macro made functions read like a description of the happy path with the failures handled out of band:

fn load_config(path: &Path) -> Result<Config, ConfigError> {
    let raw = fs::read_to_string(path)?;
    let cfg = toml::from_str(&raw)?;
    Ok(cfg)
}

That reads like what it does. The pre-? version of the same function was three times the length and most of it was ceremony. Once you have written enough of these, you start designing your error types deliberately so that ? flows cleanly through them, and that nudges your whole approach to error handling in a healthier direction.

Then there was async/await arriving on the back of the edition work, which I will be honest took me a while to actually adopt in anger. But the groundwork the 2018 edition laid, the lifetime and borrow improvements, non-lexical lifetimes in particular, made a lot of code that the borrow checker used to reject for no good human reason simply compile. NLL alone removed a category of fights I used to have with the compiler where I knew the code was correct and it knew the code was correct and we still had to argue about scopes.

What strikes me looking back is that none of these were flashy. There was no single feature you could point at and say "that is why I write Rust differently now." It was the accumulation: better modules, cleaner error propagation, a borrow checker that argued less, paths that made sense. Together they shifted Rust from a language I admired and occasionally fought, to one I reached for by default. The editions since have each done their own version of this, smoothing edges without breaking old code, and I have come to think that steady, backwards-compatible polish is one of the most underrated things about the language.

The old crate still compiles, by the way. I bumped its edition while I was in there, fixed the two warnings that produced, and moved on. That it was only two warnings, years later, is rather the point.