Rust 1.95.0: Key Changes and Upgrade Verdict

intermediate 7 min read updated 2 Aug 2026
On this page 5

Rust 1.95.0: Upgrade Verdict

Upgrade now. This release includes a crucial fix for a memory leak affecting async applications and significant performance improvements for HashMap. These changes provide immediate benefits with minimal risk.

The memory leak fix addresses an issue where Futures created with specific async fn patterns could retain memory longer than expected. Teams using async Rust, particularly in long-running services or embedded systems, will see reduced memory footprint and improved stability.

HashMap operations now execute faster due to internal optimizations. Benchmarks show up to a 15% reduction in average lookup times for large maps. Applications relying on extensive map usage, such as data processing pipelines or caching layers, will experience a direct performance gain without code changes.

This release also stabilizes portable_simd for x86_64 targets. Developers working on high-performance numerical computing can now use SIMD intrinsics directly in stable Rust, simplifying cross-platform vectorization efforts previously requiring nightly features or external crates. To use it, import from std::simd:

use std::simd::{Simd, f32x4};

fn process_simd(a: f32x4, b: f32x4) -> f32x4 {
    a * b + Simd::splat(1.0)
}

No known regressions or breaking changes are reported that would impact typical stable Rust projects. The upgrade path is straightforward.

Affected users include anyone running async Rust services, applications with heavy HashMap usage, and developers targeting SIMD for performance-critical computations.

To upgrade, run:

rustup update stable

For projects with strict dependency trees or long-term support requirements, a staged rollout is always prudent. However, the benefits of the memory leak fix and HashMap performance improvements outweigh the minimal upgrade risk for most teams.

Match Arms: Stabilized if let Guards

Rust 1.95.0 stabilizes if let guards within match arms. This feature allows you to introduce new variables and apply further pattern matching directly within a match arm’s guard condition. Previously, such complex conditions often required nesting match statements or creating tuples to match against multiple values.

Consider a scenario where you need to match an Option<T> and then conditionally check another Option<U> based on the first match. Before this stabilization, you might write code like this:

enum Status { Active, Inactive }
struct User { id: u32, status: Status }
fn process_user_data(user_opt: Option<User>, config_id: Option<u32>) {
    match user_opt {
        Some(user) => {
            if let Some(id) = config_id {
                if user.id == id && matches!(user.status, Status::Active) {
                    println!("Processing active user {} with matching config ID {}", user.id, id);
                }
            }
        }
        None => println!("No user data"),
    }
}

With if let guards, the condition becomes part of the match arm itself. This keeps the logic for a specific branch more localized and readable. The config_id value is pattern-matched directly in the guard, making the id variable available within that arm.

enum Status { Active, Inactive }
struct User { id: u32, status: Status }
fn process_user_data_new(user_opt: Option<User>, config_id: Option<u32>) {
    match user_opt {
        Some(user) if let Some(id) = config_id && user.id == id && matches!(user.status, Status::Active) => {
            println!("Processing active user {} with matching config ID {}", user.id, id);
        }
        Some(_) => println!("User found, but conditions not met"),
        None => println!("No user data"),
    }
}

This syntax reduces nesting and clarifies the intent. The id variable is bound only for the arm where the if let guard succeeds. This feature is particularly useful when dealing with multiple Option or Result types that need to be unwrapped and checked together in a single match expression. It makes complex pattern matching more concise and easier to follow, improving code clarity.

PowerPC: Inline Assembly Support

Inline assembly for PowerPC and PowerPC64 targets is now stable in Rust 1.95.0. This change allows developers to embed architecture-specific assembly instructions directly within Rust code using the std::arch::asm! macro. This capability is relevant for low-level programming, operating system development, and embedded systems where direct CPU interaction is necessary.

The stabilization applies to powerpc-unknown-linux-gnu, powerpc64-unknown-linux-gnu, and powerpc64le-unknown-linux-gnu targets. Previously, achieving similar control required unstable features, external assembly files, or the deprecated llvm_asm! macro. The new asm! macro offers a type-safe and more integrated approach.

Using inline assembly provides precise control over CPU operations, which can be useful for optimizing critical code paths or interacting with hardware registers directly. For example, a memory barrier or a specific instruction not exposed via compiler intrinsics can be emitted:

#[cfg(target_arch = "powerpc64")]
unsafe {
    // Example: A simple no-operation instruction
    std::arch::asm!("nop");

    // Example: Using inputs and outputs for a simple addition
    let a: u64 = 10;
    let b: u64 = 20;
    let mut c: u64;
    std::arch::asm!(
        "add {0}, {1}, {2}", // PowerPC assembly for add: D = A + B
        out(reg) c,          // Output register for c
        in(reg) a,           // Input register for a
        in(reg) b,           // Input register for b
    );
    // After execution, c will hold the value 30.
}

While inline assembly offers fine-grained control, it introduces platform-specific code. This can reduce portability and increase the complexity of debugging, as the compiler has less visibility into the assembly block’s behavior. Use it when Rust’s higher-level abstractions or std::arch intrinsics do not provide the needed functionality or performance.

Minor Language Enhancements

Rust 1.95.0 introduces a new lint, irrefutable_let_patterns, which warns when a let binding uses a pattern that cannot fail. This often signals a logical error where if let or while let was intended. The lint defaults to warn and helps catch common mistakes during development.

For example, this code will now trigger a warning:

fn main() {
    let Some(x) = Some(1); // This will warn
    println!("{}", x);
}

The compiler suggests using let x = Some(1).unwrap(); or let x = 1; for clarity when a let pattern is irrefutable. This improves code readability and prevents accidental panic! scenarios if the type of Some(1) were to change later to something that could be None.

Constant evaluation saw further consistency improvements. The compiler now handles more complex expressions within const contexts, reducing cases where an expression valid in runtime code would fail during const evaluation. This change primarily affects library authors and those writing const fn or const items, enabling more expressive compile-time computations. Specifically, operations like certain bitwise manipulations on integer types or more intricate array initializations are now permitted in const blocks. This reduces reliance on lazy_static or similar runtime initialization patterns for data structures that can logically be prepared at compile time.

This release also includes minor adjustments to how #[track_caller] interacts with specific macro expansions. This ensures more accurate caller information in error messages, which is particularly relevant for macro authors. Improved diagnostics from macros using #[track_caller] benefit all users by pointing to the exact call site more reliably.

These enhancements collectively make the language more predictable and guide developers towards safer, more idiomatic patterns. They are not breaking changes but rather refinements that improve developer experience and compiler diagnostics.

Migration and Breaking Changes

Rust 1.95.0 hardens the deprecation of std::error::Error::description and std::error::Error::cause. These methods now result in a compilation error if overridden or called. Previously, they were warnings.

Projects implementing std::error::Error must update their error types. Replace description logic with the std::fmt::Display implementation. For cause, use the source() method. This change affects older error implementations that have not yet migrated to the modern Error trait patterns.

Consider this example for an affected error type:

use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct MyError;

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "A custom error occurred")
    }
}

impl Error for MyError {
    // This method will now cause a compilation error
    fn description(&self) -> &str {
        "Old description"
    }
    // This method will also cause a compilation error
    fn cause(&self) -> Option<&dyn Error> {
        None
    }
}

To resolve this, remove the description and cause overrides. The Display implementation covers the descriptive aspect, and source() handles chaining.

A new warning, clippy::redundant_clone, is now enabled by default for cargo check. This lint flags unnecessary clone() calls, particularly on Copy types or when a move would suffice. While not a compilation error, it will introduce new warnings into affected codebases.

If your CI pipeline treats warnings as errors (-D warnings), this change will break builds. Review the output from cargo clippy and remove redundant clone calls. This change affects projects with existing code that contains such patterns.

Example of code that will now warn:

fn process_number(n: i32) {
    // This clone is redundant as i32 is Copy
    let _ = n.clone();
}

Verdict: Upgrade now. The breaking change regarding Error::description and cause is a long-standing deprecation finally enforced; most projects should have migrated. The new clippy warning is easily addressed. Projects using deny(warnings) in CI should anticipate and resolve the redundant_clone warnings post-upgrade.