February 20, 2025·6 min read·engineeringrusteditionsasyncsafety

Rust 2024 makes compatibility changes explicit

Rust 1.85 stabilizes the 2024 edition and async closures. The edition provides a boundary for revised safety obligations and lifetime defaults, while migration tools preserve existing behavior conservatively.

A function can have obligations that its original type signature failed to express. Changing that signature later can break callers even when the correction makes the interface more accurate. Rust's edition mechanism provides an opt-in boundary for such changes, allowing new source rules without requiring every existing crate to adopt them together.

Rust 1.85, released on February 20, stabilizes the 2024 edition. The release also stabilizes async closures, which address borrowing and callback signatures that an ordinary closure returning an async block cannot fully express. These are related improvements to the precision of Rust code, but the toolchain release and a crate's edition remain different choices.

I find Rust compelling where a systems interface needs its assumptions stated in the language. The 2024 changes do not remove that work. They make several previously implicit obligations visible at a source boundary where maintainers can review them deliberately.

An edition is a migration boundary

The release announcement describes editions as opt-in changes that might otherwise pose compatibility risks. Updating the compiler does not by itself mean every crate changes edition. This distinction allows the ecosystem to move incrementally, while the same compiler supports code written under established source rules.

The 2024 edition changes language rules and some standard-library interactions. It also changes Cargo's default dependency resolver to consider declared compiler compatibility. Keeping these under a named edition gives maintainers a coherent point at which to inspect their consequences, rather than introducing every incompatible interpretation into old source automatically.

Migration tooling serves a conservative purpose. The announcement says cargo fix --edition attempts to preserve behavior. Its output is not a recommendation that the old behavior is preferable. A maintainer can choose new semantics deliberately, but an automatic transformation should not silently substitute them merely because the new edition permits them.

That distinction matters for code whose correctness depends on scope or drop timing. A mechanical migration must retain its earlier behavior where possible. Adopting a different lifetime or temporary scope is a separate source-level decision, even when the resulting code is shorter.

Calling an unsafe function and implementing it are different duties

The edition guide's unsafe-operation change makes unsafe_op_in_unsafe_fn warn by default. An unsafe function states that its caller must uphold extra conditions. Its implementation still needs explicit unsafe blocks around operations that rely on those conditions.

Previously, the unsafe function marker could play both roles. It described the caller's obligation and implicitly allowed unsafe operations throughout the body. Separating those roles gives a reviewer smaller regions in which to inspect the implementation's reasoning. A later edit cannot acquire permission merely by being inside a long unsafe function without producing the new default warning.

An unsafe block is not itself a proof. Its value is to identify where the proof belongs. Comments and the function's documented preconditions must explain why the operation is valid. Adding a large block around the entire body can satisfy a diagnostic while doing little to improve the precision of that explanation.

The change also helps distinguish a safe implementation detail inside an unsafe function from the operation that consumes an extra guarantee. A function can perform ordinary validation before using a raw pointer. The explicit block identifies the access whose correctness depends on what the caller promised.

The environment has platform safety constraints

The newly unsafe functions guide changes the calling requirements for std::env::set_var and remove_var in the new edition. Process-environment mutation has safety limitations on some platforms when other threads may be accessing the environment. The original safe declarations could not express those limitations.

This is more than a request for extra punctuation. A migration can wrap a call in an unsafe block, but the tool cannot establish that no other thread might be running. A maintainer must examine the operation's actual preconditions or choose a different design. Source compatibility and memory-safety justification are separate outcomes.

The same chapter covers the Unix before_exec method, which was already deprecated in favor of an unsafe replacement. Rust 2024 marks the older method unsafe as well. Its callback runs in a context with strict restrictions, so an ordinary-looking closure is not permission to perform arbitrary library operations there.

These changes show why an edition can correct an interface without pretending that older source was universally safe. The underlying platform conditions existed before the marker changed. The new source requirement gives callers a visible place to acknowledge and justify them.

Opaque return types capture lifetimes more consistently

The return-position impl Trait guide describes the new default lifetime capture rule. In Rust 2024, an opaque return type implicitly captures in-scope generic parameters, including lifetimes, when a precise use<...> bound is absent.

Capturing a parameter permits the hidden return type to use it. The capture set also affects how the caller can use the opaque result, even if the hidden implementation does not happen to store that lifetime. This is a contract visible through the signature, not simply an observation about one function body's current layout.

Precise capture bounds let an author state the intended set explicitly. Migration tools can add them to preserve older behavior when the new default would capture more. The benefit is a direct way to describe the relationship instead of relying on indirect lifetime tricks whose meaning requires reconstructing earlier compiler rules.

For API design, this makes lifetime review part of edition migration. A function returning an opaque type might retain a tighter connection to an input under the new default. Whether that connection belongs in the public contract depends on the operation, not on a blanket preference for fewer bounds.

Async closures can borrow their captures

RFC 3668 explains two limitations that motivated async closures. An ordinary closure returning a future cannot generally let that future borrow from the closure's captured state. Expressing a callback whose returned future depends on an input lifetime also becomes awkward with a separately named future type parameter.

The stabilized async || form and the AsyncFn, AsyncFnMut, and AsyncFnOnce traits address those relationships. Stabilized examples use the named trait family in bounds. RFC discussion includes earlier proposed spelling, so examples need to follow the stabilized release rather than copy every design-stage syntax literally.

async fn visit(callback: impl for<'a> AsyncFn(&'a str)) {
    callback("first").await;
    callback("second").await;
}

This function uses the async callable trait available in the prelude. It awaits each call before making the next. It does not launch concurrent tasks or provide an executor. Those responsibilities remain separate from describing the relationship between a callback argument and its future.

An async closure's body executes through its returned future. Its ability to borrow captured state is useful precisely because the type system can represent that relationship. It avoids requiring unnecessary ownership transfer merely to fit a callback into an ordinary closure-returning-future pattern.

I prefer this improvement to hiding borrowing requirements behind a helper that clones everything. Ownership decisions should follow the operation's actual lifetime needs. Async closures give the language a direct way to express a useful relationship that ordinary closure traits could not describe adequately.

Formatting can move independently

The release introduces formatting style editions as another independent choice. A language edition determines how source is interpreted, while a style edition determines formatting conventions. Separating them lets a project manage formatting changes without treating a large mechanical diff as a necessary consequence of every semantic migration.

That distinction matters for review because whitespace changes and behavior changes require different attention. A maintainer can inspect a new source rule in a focused diff, then review formatting changes under a separate policy. The release also contains formatting corrections and changes to identifier sorting, so retaining an explicit style choice helps explain why a formatter produces a different result after an upgrade.

Migration establishes the intended contract

Rust 2024's migration tools can identify many mechanical source changes, including explicit unsafe regions and precise capture bounds. They cannot determine every intended application invariant. A caller still needs to justify environment mutation, and a library author still needs to decide which input lifetimes an opaque result should expose.

The release makes those decisions more explicit while retaining an incremental adoption mechanism. Existing crates can remain on their edition as maintainers inspect the changes. New code can use async closures through the stabilized traits, and migrated code can state safety and lifetime relationships under the new rules without requiring a coordinated ecosystem-wide source change.