Rust 1.96 is out, finally solving the problem of non-copyable ranges and stabilizing the highly anticipated assert_matches! macro natively in the standard library

The release of Rust 1.96.0 (May 2026) is a perfect example of the language’s commitment to fixing deep-seated ergonomic issues. While it includes several compiler improvements and crucial Cargo security patches, the real stars of this release are the features that remove daily friction for developers.

If you have ever tried to pass a slice range into a function and hit a frustrating borrow-checker wall, or if you are tired of writing verbose match blocks just to assert an enum variant in your tests, 1.96 was release just for you.

Here is a breakdown of the most impactful changes in this release.

Fixing Copy Ranges

Since the early days of Rust 1.0, ranges like 0...5 or start...end have been represented by types in std::ops (like std::ops::Range). These types implement Iterator, which mutates its internal state as you loop through it.

Because iterating mutates the range, the core developers historically decided that ranges could not implement the Copy trait. Doing so would risk bugs where developers accidentally copied a partially consumed iterator.

However, this created a massive annoyance for API desing. If you wanted to store a range inside a struct that implemented Copy (like a simple span of text), you couldn’t. You had to pass ranges by reference or manually extract the start and end indices.

The 1.96 Solution

Rust 1.96 resolve this by introducing an entirely new set of range types housed under core::range (e.g., core::range::Range or core::range::RangeForm).

These new types cleanly separate the concept of “a boundary of values” from the concept of “an iterator”. Because the new range types are simply data boundaries, they finally implement Copy.

This means you can now seamlessly store ranges in Copy types without splitting the start and end values:

use core::range::Range;

#[derive(Clone, Copy)]
pub struct Span(Range<usize>);

impl Span {
    pub fn extract<'a>(self, text: &'a str) -> &'a str {
        &text[self.0] // This now works effortlessly.
    }
}

Library authors are heavily encouraged to start transitioning public APIs to accept impl RangeBounds, which gracefully handles both the legacy and new range types as the ecosystem migrates.

Cleaner Tests with assert_matches!

Testing enums in Rust has always been slightly more verbose that it needed to be. If you had a function returning an Option or a custom Result and you only wanted to assert that it matched a specific variant, you usually had to write somethig like this:

let response = process_payload(&data);

// The old, verbose way
match response {
    Message::Success(id) => assert_eq!(id, 42),
    _ => panic!("Expected a Success message!"),
}

Rust 1.96 stabilizes the assert_matches! and debug_assert_matches! macro natively in the standard library. This allows you to test patterns in a single, highly readable line:

use std::assert_matches::assert_matches;

let response = process_payload(&data);

// The 1.96 way
assert_matches!(response, Message::Success(id) if id == 42);

If the pattern doesn’t match, the macro automatically panics with a clean, formatted error message showing exactly what was expected versus what was received. It is a small change, but if you maintain a large test suite, this will strip hundreds of lines of boilerplate out of your codebase.

Stricter WebASsembly Linking

For developers working on the edge or compiling Rust to Wasm, 1.96 tightens up the build process.

Previously, if your WebAssembly build had undefined symbols during linking, the compiler would quitly pass —allow-undefined to the linker, converting those missing symbols into WebAssembly environment imports. This often led to confusing runtime errors if those imports weren’t actually provided by the host environment.

As of 1.96, undefined symbols are treated strictly as linker errors. If a function is missing, the build fails immediately. This forces developers to explicitly define their Wasm imports and catches architecture bugs long before the code hits the browser or serverless runtime.

Sum It Up

Rust 1.96 is a cleanup release in the best possible way. By finalizing the new Copy range types and bringing assert_matches! into the standard library, it actively removes the minor frustrations that interrupt a developer’s frow state.

Find more technical deep-dives on the Rust ecosystem and modern software architecture at rust-stack.com