Upgrading to Rust 1.95: Breaking Changes I Actually Hit in Production

Every six weeks I go through the same ritual: run rustup update stable, let the build finish, and wait to see what quietly stops compiling. Most releases pass without drama. 1.95 wasn’t one of those. It shipped on April 16, 2026, and while the changelog reads like a routine list of internals cleanup, three of the items on it actually touched code I maintain, and one of them cost me an afternoon tracing a macro failure through three layers of crate re-exports before I found the real cause.

What follows isn’t a copy of the release notes with commentary bolted on. It’s the subset of changes that mattered when I moved a mid-sized workspace from 1.94 to 1.95, why they mattered, and what I’d check first if you’re doing the same jump.

The workspace in question is nothing exotic: a handful of internal crates, a couple of proc macros written a few years ago and never touched since, and one FFI layer talking to a C library over a packed struct. None of that sounds like it should be fragile. That’s exactly why this release stood out to me. The breakage wasn’t in application logic, it was in the connective tissue, macros, const evaluation, module resolution, the stuff you write once and never look at again until a compiler update forces you to.

If Let Guards Finally Close a Gap I’ve Been Annoyed About for Years

The headline language feature in 1.95 is the stabilization of if let guards inside match arms. This isn’t a breaking change on its own, but it changed how I write code the moment I updated, which is worth mentioning because it reshapes what “idiomatic” looks like going forward, and reviewers on your team will start expecting it.

Before 1.95, combining a match guard with a nested pattern meant either a helper function or an ugly nested match. I had exactly this pattern in a UI event dispatcher, where one error variant needed special handling only when an inner field matched a specific role:

// old approach, pre-1.95
match get_window_id(&element) {
    Ok(id) => Some(id),
    Err(e) => {
        if let Ok(role) = element.role() {
            if role == Role::ScrollArea {
                None
            } else {
                log_error(e);
                None
            }
        } else {
            log_error(e);
            None
        }
    }
}

With 1.95, the same logic collapses into one arm:

match get_window_id(&element) {
    Ok(id) => Some(id),
    Err(_) if let Ok(role) = element.role() && role == Role::ScrollArea => None,
    Err(e) => {
        log_error(e);
        None
    }
}

This isn’t going to break your build. It’s going to make old code look outdated fast, and if you’re reviewing pull requests, expect to see this pattern show up unannounced. I’d flag it in your team’s style notes before someone introduces it inconsistently across a codebase.

Deep Dive
Rust Clone vs Arc...

Clone, Arc, and Lifetime Annotations: Why Your Rust Architecture Is Quietly Bleeding Performance Most mid-level Rust devs hit the same wall: the compiler shuts up, the tests pass, and production quietly burns CPU cycles on...

There’s a secondary effect worth naming: clippy will start suggesting this pattern once it catches up, and that suggestion is going to fire on code that was perfectly fine before. If you’re running clippy with warnings-as-errors in CI, budget time for a pass where you either accept the new lint or explicitly allow it, rather than letting a routine dependency bump turn into a blocked pipeline the day someone updates the toolchain image.

The $crate Self-Import Change That Silently Killed a Macro-Heavy Crate

This is the one that actually broke my build. 1.95 tightens the rules around self-imports, and specifically disallows importing $crate without renaming it, something like:

macro_rules! reexport_helpers {
    () => {
        use $crate::{self};
    };
}

I had a macro in an internal utility crate that did exactly this as a lazy way to bring the crate root into scope inside generated modules. It had worked fine since roughly 2023 and nobody had touched it since. Under 1.94 it compiled without a warning. Under 1.95, every macro invocation using it failed with a self-import error, and because the failure surfaced inside macro-expanded code, the compiler pointed at the invocation site rather than the macro definition. I spent most of an hour assuming the bug was in the calling code before I actually opened the macro itself.

The fix is trivial once you find the source:

macro_rules! reexport_helpers {
    () => {
        use $crate as root;
    };
}

If your codebase has any macro-heavy internal tooling written more than a year or two ago, grep for $crate::{self} and use $crate::{self as before you upgrade, not after. This is exactly the kind of change that doesn’t show up in a diff review because the macro definition itself didn’t change, only what the compiler will accept when it’s expanded.

What made this particular failure frustrating wasn’t the fix, it was the diagnosis path. The compiler error referenced the macro invocation, not the macro body, so my first instinct was to check every call site for something wrong with how it was being used. It took deliberately stripping the macro expansion down with cargo expand before the actual source of the error became visible. If you hit a self-import error after upgrading and the flagged line looks completely unremarkable, don’t trust the line number, trust the expansion.

Const-Eval Padding Consistency: A Corner Case That Only Bites Unsafe Code

1.95 makes const evaluation more consistent about how it handles padding bytes during typed copies. In practical terms, if you have a const or static built from a struct with padding, and something in your evaluation path was previously reading undefined padding bytes inconsistently between compile-time and runtime, that inconsistency is now resolved, and in rare cases this surfaces as a new compile error rather than a silent behavior mismatch.

I only hit this because of a bit-packing struct used for a binary protocol parser, where a const table was built from a repr(C) type with explicit alignment padding:

#[repr(C)]
struct Header {
    flag: u8,
    // 3 bytes of padding here on most platforms
    id: u32,
}

const DEFAULT_HEADER: Header = Header { flag: 0, id: 0 };

Nothing here is inherently wrong, and most people will never see this. But if you’re doing anything with manually laid out structs in const context, particularly around FFI boundaries, it’s worth a targeted build after upgrading rather than assuming this category of code is unaffected. The failure mode, when it happens, is a compile error rather than a subtle runtime bug, which is honestly the better outcome, but it’s still a build that worked yesterday and doesn’t today.

Pattern Matching Semantics Now Independent of Crate and Module Boundaries

This is a compiler-internals change dressed up as a small note in the changelog, but it matters if you write proc macros or work close to pattern matching internals. Previously, the exact operational semantics of certain pattern matches could differ subtly depending on which crate or module the match occurred in, an artifact of how the compiler resolved things during lowering rather than an intentional language feature. 1.95 makes this behavior uniform.

For almost everyone this is invisible. Where it bites is generated code that relied, even accidentally, on module-specific matching quirks:

// generated by a derive macro, simplified
match &self.state {
    State::Active { ref data, .. } if data.is_valid() => handle_active(data),
    _ => handle_default(),
}

If a derive macro you depend on generates match arms like this and behaves differently after the upgrade, the fix isn’t in your code, it’s in the macro crate. Check for updates to any custom derive macros in your dependency tree before assuming your own logic is at fault. I mention this mainly because it’s the kind of bug that sends you searching your own code for hours when the actual fix is a cargo update on a transitive dependency.

Technical Reference
Rust Developer

Common Rust Developer Pain Points and How to Solve Them Rust fights you before it trusts you. The borrow checker rejects code that looks fine. Async tasks run sequentially when you expect concurrency. FFI boundaries...

The Ambiguous Glob Import Lint You Should Treat as an Error Today

1.95 introduces ambiguous_glob_imported_traits as a future-incompatibility lint. It’s a warning right now, which means your build still passes, but it’s the compiler’s way of telling you this will eventually stop compiling. It triggers when a glob import brings in a trait whose resolution is ambiguous relative to another explicit or glob-imported trait:

use crate::traits::*;
use another_crate::formatting::*;

// if both modules export a trait with the same method name,
// resolution used to pick one silently — now it warns
fn describe(item: &Item) -> String {
    item.format()
}

I treat every future-incompatibility lint the same way: fix it now while you have the context, not later when the crate has grown and the ambiguity is buried three refactors deep. Run cargo build 2>&1 | grep ambiguous_glob across your workspace after upgrading and clear these out immediately rather than letting them accumulate as warnings nobody reads.

The reason I’m strict about this specific lint is that glob imports tend to multiply quietly. A module that starts with one wildcard import ends up with three or four by the time a project matures, and each new one is a chance for a trait method collision the compiler previously resolved by convention rather than by explicit rule. Waiting on this warning means the eventual fix has to happen across a much wider surface, usually right when a future stable release turns it into a hard error with no notice beyond a changelog entry most teams don’t read line by line.

Lifetime Bounds Got Stricter for Type-Parameter-Only Types

The compiler now checks lifetime bounds more thoroughly for types that mention only type parameters, closing a gap where certain generic types could previously sidestep lifetime bound checks the compiler should have been enforcing. This is a soundness fix, not a stylistic change, and the Rust team explicitly frames these kinds of updates as low blast-radius. In my own workspace this affected exactly one generic wrapper type, and the fix was adding an explicit bound rather than relying on inference:

// before: bound was implicitly assumed, not checked
struct Wrapper {
    inner: T,
}

// after: explicit bound required where lifetimes are involved
struct Wrapper {
    inner: T,
}

If you maintain generic library code with lifetime-sensitive wrappers, this is worth a dedicated pass, but for application code consuming those libraries, you’ll likely never notice unless the library itself needs updating.

What I appreciate about this kind of fix, even when it costs me an hour, is that it’s closing a gap rather than opening one. The compiler was letting something through that it should have rejected, and the Rust team’s own framing of soundness fixes as low blast-radius has held up in practice every time I’ve hit one. Compare that to ecosystems where a point release can silently change runtime behavior with no compile-time signal at all, and an explicit build error, however inconvenient, is the better failure mode by a wide margin.

What I’d Actually Check Before Running cargo update

Comparing this cycle to older jumps like 1.82 to 1.83, which was mostly about const-stable float operations and had almost nothing that could break existing code, 1.95 is a heavier release specifically because of the self-import tightening and the const-eval padding change. Neither is dramatic in isolation, but together they’re exactly the kind of thing that passes CI on a small project and fails quietly on a larger one with more macro usage and more unsafe FFI code.

Worth Reading
Rust Development

Rust Development Tools: From Cargo to Production-Grade Workflows Most teams adopt Rust for its safety guarantees, then spend the next six months fighting compile times, misconfigured linters, and a debugger that doesn't speak "borrow checker."...

Before I moved my own workspace over, I did four things, in this order:

  • Grepped every crate for $crate::{self} and any macro that re-exports the crate root without renaming.
  • Ran a full build with -W ambiguous_glob_imported_traits treated as visible output, not suppressed, and cleared every hit.
  • Isolated any repr(C) or manually padded structs used in const context and rebuilt just those modules first, in isolation, before touching the rest of the workspace.
  • Updated derive macro dependencies before assuming my own pattern matching logic was broken.

None of this took more than the afternoon I mentioned at the start, and most of it was investigation rather than actual code changes. That’s the pattern with Rust’s release cycle in general: the breaking changes are real, but they’re narrow, well-documented, and rarely require a rewrite. The cost isn’t fixing the code, it’s finding which corner of a large codebase the change actually touches.

There’s a broader lesson buried in this release that I keep coming back to. Every one of the changes above was flagged in the draft release notes weeks before 1.95 actually shipped, sitting in a public GitHub issue with a tracking link. I didn’t read that issue in advance, and that’s on me, not on the process. Rust’s release train is unusually transparent about what’s coming, and the six-week cadence means the changes per release stay small enough to actually review if you make a habit of it. The mistake I made this cycle wasn’t missing a subtle compiler behavior, it was treating a routine rustup update as routine when a quick skim of the draft notes would have caught the self-import change before it caught me.

If you keep a short checklist like the one above and run it every six weeks instead of waiting for a painful jump across several versions at once, the whole process stays boring, which for a compiler upgrade is exactly what you want. The alternative, batching four or five point releases into one upgrade because you never got around to it, is how a routine afternoon turns into a lost week.

If you’re reading this after moving your workspace to 1.96, a few things I’ve described above shift in scope. The self-import restriction I hit with macros in 1.95 got extended to plain struct imports in the next release, so codegen pipelines are worth a second look, not just macro-heavy crates. WebAssembly builds also picked up a stricter linker check around implicit symbol imports, which caught more than one team off guard the same week it shipped. I went through the full breakdown, including the fix we used and why the green CI run couldn’t be trusted that day, in a separate write-up on what actually broke in Rust 1.96.

Written by:

Source Category: Rust Engineering