Rust 1.96.0: Key Changes and Upgrade Verdict

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

Rust 1.96.0: Upgrade Verdict

Rust 1.96.0 introduces a new #[must_use] lint for Result types returned by async fns. This lint identifies cases where the Result from an asynchronous operation is ignored, potentially masking errors. Projects using async/await will see new warnings if they do not explicitly handle or ? propagate these Results.

For example, the following code will now trigger a warning:

async fn do_something_risky() -> Result<(), String> {
    Err("failed".to_string())
}

async fn main_task() {
    do_something_risky().await; // Warning: `#[must_use]` value not used
}

This change affects all developers working with asynchronous Rust, especially those maintaining larger codebases. Addressing these warnings will improve application reliability by enforcing explicit error handling.

The compiler also includes performance improvements for async fn state machines. This release reduces the memory footprint and improves compile times for projects with many complex asynchronous functions. Internal benchmarks show a 3-5% reduction in binary size for tokio-heavy applications and up to 2% faster incremental compilation for such crates. This optimization is transparent and benefits all async users without requiring code modifications.

Another notable addition is the stabilization of BTreeMap::first_entry and BTreeMap::last_entry. These methods provide direct access to the Entry API for the smallest and largest elements in a BTreeMap, simplifying operations like atomic removal or update.

use std::collections::BTreeMap;

let mut map = BTreeMap::new();
map.insert(3, "c");
map.insert(1, "a");

if let Some(entry) = map.first_entry() {
    println!("First entry: {:?}", entry); // Output: First entry: OccupiedEntry { key: 1, value: "a" }
}

This feature is useful for developers managing ordered key-value data structures, offering a more ergonomic approach compared to iterating or using key-based min/max methods.

Verdict: Upgrade Now.

This release contains crucial lints that improve code correctness for async applications and delivers transparent performance gains for async fns. The BTreeMap additions are minor but welcome. There are no known regressions or breaking changes that warrant delaying an upgrade. Update your toolchain using rustup update stable to benefit from these improvements and increased stability.

cfg Metavariable: Macro Flexibility

Rust 1.96.0 allows cfg attributes to accept expr metavariables within declarative macros (macro_rules!). Previously, cfg predicates were restricted to literal identifiers, paths, or specific cfg syntax directly. This limitation meant macro authors could not dynamically construct conditional compilation predicates based on their macro inputs.

This change primarily affects macro authors. It enables more flexible and less repetitive conditional compilation logic directly within macros. Developers can now write macros that take a feature name, platform target, or other configuration as an argument and use that argument to gate generated code. This removes the need for workarounds or repetitive manual cfg attribute application when generating multiple similar items.

Consider the challenge of creating a macro that defines items conditionally based on a feature name passed to it. Before this release, the feature string would have to be hardcoded or the conditional logic would sit outside the macro. Now, an expr metavariable can directly form part of the cfg predicate.

// Rust 1.96.0 and later
macro_rules! define_feature_gated_item {
    ($feature_name:expr, $item_name:ident) => {
        #[cfg(feature = $feature_name)]
        struct $item_name;
    };
}

define_feature_gated_item!("my_custom_feature", MyFeatureStruct);
define_feature_gated_item!("another_feature", AnotherStruct);

// This expands to:
// #[cfg(feature = "my_custom_feature")]
// struct MyFeatureStruct;
// #[cfg(feature = "another_feature")]
// struct AnotherStruct;

This capability simplifies the creation of abstractions that manage feature flags or target-specific implementations. It reduces boilerplate when generating multiple items that share similar conditional compilation requirements but differ only in the specific predicate value. The tradeoff is that highly dynamic cfg predicates can make it harder to quickly understand compilation paths or debug issues without inspecting macro expansion.

ManuallyDrop Pattern Fix

Rust 1.95.0 introduced a regression that permitted ManuallyDrop constants to be used directly as patterns. This behavior was unintended and has been corrected in Rust 1.96.0, restoring the original safety guarantees.

ManuallyDrop is a wrapper type designed for advanced memory management scenarios. It prevents the compiler from automatically calling drop on its inner value when the ManuallyDrop instance goes out of scope. This is essential for managing resources that require custom deallocation logic, or for ensuring a value is moved out without being dropped.

The regression in 1.95.0 allowed code like let ManuallyDrop(value) = MY_CONST; where MY_CONST was a const item of type ManuallyDrop<T>. When such a pattern was matched, the inner value would be moved out. This left the original ManuallyDrop constant in an invalid state, potentially leading to use-after-free issues or double-frees if drop was called on the constant later. This undermined the core purpose of ManuallyDrop.

Consider this example, which would compile in Rust 1.95.0 but now produces an error in 1.96.0:

use std::mem::ManuallyDrop;

struct MyResource;

impl Drop for MyResource {
    fn drop(&mut self) {
        println!("MyResource dropped!");
    }
}

const RESOURCE: ManuallyDrop<MyResource> = ManuallyDrop::new(MyResource);

fn main() {
    // This pattern match was allowed in 1.95.0,
    // moving out the inner MyResource.
    // In 1.96.0, this causes a compilation error.
    let ManuallyDrop(r) = RESOURCE;
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // error[E0507]: cannot move out of `RESOURCE` which is behind a `const` item
    // note: move occurs because `r` has type `MyResource`, which does not implement the `Copy` trait
}

In Rust 1.96.0, the line let ManuallyDrop(r) = RESOURCE; generates a compiler error, preventing the unsafe extraction of the inner value from a ManuallyDrop constant via pattern matching.

This fix primarily affects developers who use ManuallyDrop with const or static items, particularly in scenarios requiring precise control over resource lifetimes. If your code relied on the ability to pattern match ManuallyDrop constants in 1.95.0, it will now require adjustment. The correct way to interact with the inner value of a ManuallyDrop constant is through methods like ManuallyDrop::into_inner() or by re-wrapping it, ensuring the drop behavior is explicitly managed.

This change is an important correction for memory safety. It reinforces the contract of ManuallyDrop, ensuring that its inner value is only deallocated or moved out through explicit, safe operations.

Never Type Coercion Improves

Rust 1.96.0 introduces more consistent never type (!) coercion within tuple expressions. Previously, the compiler’s behavior when ! appeared as a tuple element was inconsistent, sometimes leading to type errors that required explicit casting.

The never type ! represents computations that never return, such as panic!() or loop {}. Rust’s type system allows ! to coerce to any other type, as a function that never returns can logically be considered to return any type. This property is important for handling control flow that exits early.

Before this release, an expression like (1, panic!()) might fail to compile if the expected type for the second tuple element was String, even though panic!() (which has type !) should be coercible to String. The compiler would sometimes treat the ! as a distinct type within the tuple element, preventing the implicit conversion.

Consider this example:

fn get_pair(condition: bool) -> (u8, String) {
    if condition {
        (0, "success".to_string())
    } else {
        // Before 1.96.0, this could be a type error.
        // `panic!()` is `!`, but the compiler might not coerce it to `String` here.
        (1, panic!("failure"))
    }
}

With Rust 1.96.0, the compiler now consistently applies never type coercion to tuple elements. The panic!() expression in the example above will correctly coerce to String, allowing the code to compile without issues. This change aligns tuple element coercion with how ! behaves in other contexts, such as match arms or if/else expressions.

This improvement makes the type system more predictable when working with !, especially in scenarios involving fallible operations within tuples. Developers who use panic!, todo!, or unimplemented! within tuple elements will find their code compiles more reliably and requires fewer explicit type annotations. This change removes a source of unexpected type mismatches, simplifying code that relies on ! for non-returning branches.

Who Should Upgrade Now?

Rust 1.96.0 stabilizes the #[const_trait] attribute. This allows defining const methods within traits, enabling their use in const contexts. For example:

#[const_trait]
trait MyConstTrait {
    const fn get_value(&self) -> u32;
}

struct MyStruct(u32);

impl const MyConstTrait for MyStruct {
    const fn get_value(&self) -> u32 {
        self.0
    }
}

const fn calculate_at_compile_time() -> u32 {
    let s = MyStruct(10);
    s.get_value() + 5
}

const RESULT: u32 = calculate_at_compile_time(); // Works now

Library authors and those building no_std applications that require compile-time guarantees or extensive const evaluation should upgrade immediately. This feature is valuable for improving the ergonomics and capabilities of const generics and embedded development.

The release also includes a significant performance improvement for HashMap. Benchmarks show up to a 15% speedup for workloads using small integer keys on x86-64 Linux. This optimization applies to the default hasher and requires no code changes. Teams whose applications are CPU-bound by HashMap operations, especially in high-throughput services, will see immediate benefits.

Additionally, cargo check now processes large workspaces more efficiently. Projects with over 500 crates can expect up to a 20% reduction in analysis time during development. This improves iteration speed for large monorepos.

Upgrade Verdict:

  • Upgrade Now:
    • Projects using or planning to use #[const_trait] for compile-time logic or no_std environments.
    • Applications bottlenecked by HashMap performance, particularly with small keys.
    • Large workspaces (500+ crates) that want faster cargo check times.
  • Wait:
    • Projects not affected by the above changes and prioritizing absolute stability over new features. The release does not introduce any known regressions, but waiting for patch releases is a valid strategy for non-critical upgrades.
  • Skip: Not applicable. This release contains valuable improvements and no breaking changes for stable users.