The 2018 edition shipped with Rust 1.31 back in December, and I've now had a few months and a couple of real projects to feel out how it changed things. The headline at release was "no breaking changes, just better ergonomics," which is technically true and also undersells it. Some of these changes are small enough that you stop noticing them within a week, and that's exactly the point: they removed friction I'd stopped registering as friction.
I want to write about the ones that actually changed how I structure code, rather than the full changelog. There's plenty of that elsewhere.
The module system finally makes sense
This is the big one for me. The old path rules were the single most common thing that confused people I tried to teach Rust to, and honestly they confused me often enough. The extern crate lines at the top of every file, the difference between paths that started at the crate root and paths that didn't, the mod.rs convention: none of it was hard exactly, but it was a lot of incidental ceremony for "where does this name come from."
In 2018 most of that goes away. You no longer need extern crate for the common case; a dependency in Cargo.toml is just usable. Paths in use statements now start with a clear prefix: crate:: for something in your own crate, the crate name for a dependency, self:: and super:: for relative navigation. So instead of squinting at a bare path and working out its origin, you read the first segment and you know.
// 2015 edition
extern crate serde_json;
use foo::bar::Baz; // foo relative to... where, exactly?
// 2018 edition
use serde_json::Value;
use crate::foo::bar::Baz; // unambiguously my own crate
And you can now declare a submodule with foo.rs plus a foo/ directory, instead of being forced into foo/mod.rs. That sounds trivial. It means your editor's tab bar isn't seven files all called mod.rs, which after a long day is worth more than it should be.
The practical effect is that I split modules more freely now. The old friction made me keep modules a bit larger than I wanted, because reorganising paths afterwards was tedious. Now I break things up when they want breaking up, and the imports stay readable.
Lifetimes you don't have to spell out
The second thing that's quietly changed my code is non-lexical lifetimes. The borrow checker used to tie a borrow's lifetime to its lexical scope, the curly braces, rather than to where the value was actually last used. So you'd get rejected for code that was obviously fine, write a borrow, use it, then mutate the thing afterwards, because the borrow technically "lived" until the end of the block even though nothing touched it again.
The fix used to be artificial inner scopes or shuffling lines around to appease the checker. With NLL, the borrow ends when its last use ends, which is what you meant all along. I've deleted a fair number of those defensive { } blocks I used to scatter about, and a chunk of code I'd written in a deliberately awkward order purely to keep the borrow checker happy now reads in the natural order.
It's hard to overstate how much this lowers the day-to-day tax of the language. The borrow checker still says no, but now it mostly says no to things that are actually wrong, rather than things that merely looked wrong to a slightly pedantic implementation.
? everywhere, and the shape of error handling
The ? operator isn't new to 2018, but the edition's settling-down period is when I fully committed to it, and combined with the wider stabilisation it's pushed me toward a consistent error-handling style. Functions that can fail return Result, propagate with ?, and I lean on a single crate-level error type. The old try! macro is gone from my code entirely. Reading a function top to bottom, the happy path is the code and the failures are just little ? marks, which is roughly how I think about the logic anyway.
The async groundwork
I'll be honest that I'm not writing much async yet, because the ergonomic syntax isn't stable. But the 2018 edition reserved async and await as keywords specifically so the feature could land without another breaking change, and the Future trait work has been moving steadily. So even though I'm not using it day to day, I've started structuring some of my I/O code with the assumption that it'll become async-friendly without a rewrite. That's a strange thing to design around a feature that isn't here, but the direction is clear enough to bet on.
So what actually changed
None of this is revolutionary in isolation. No single feature here is the kind of thing you'd put on a slide. But the combined effect is that Rust gets out of my way more than it used to. I split modules more, I write borrows in the order that reads best, my error handling is more uniform, and I spend noticeably less time fighting the compiler over things that were never really my mistake.
That's the nicest kind of language change: the sort where, a few months later, you go back to read some 2015-edition code and feel the friction you'd forgotten was there. The 2018 edition didn't make Rust do anything it couldn't do before. It made writing it feel less like negotiation.