AL+

Language: RO (română) EN (English)

Email us: paul@alplustech.com

Looking back at the week of 19–25 February 2025

Async callbacks should expose their borrowing contract

Rust 1.85 async closures let callbacks lend captured state across an await. Choose that interface deliberately, and keep ownership clear at task boundaries.

Citiți articolul în română

A callback that updates a collection after waiting for an operation needs an interface that can keep the right borrow alive. Requiring it to manufacture an independent future can force the caller to reorganise ownership around the library. Rust 1.85 made async closures useful precisely at that boundary. Choose them when an asynchronous call needs to borrow captured state; keep owned callbacks where work must leave that scope.

This November 2026 retrospective looks back at the 20 February 2025 release. Its stable surface included async || {} and the AsyncFn, AsyncFnMut and AsyncFnOnce traits. The design consequence is a more honest callback contract. It does not establish a performance improvement for your application or justify changing every closure that already returns a future.

The future can depend on the call

With an ordinary closure returning an async block, there are two operations to reason about: calling the closure and running the future it produces. That distinction matters when the second operation wants access to state stored in the first. A reusable callback cannot simply let a mutable reference to its own captured state escape through an arbitrary, independently described future.

RFC 3668, whose displayed start date is 25 June 2024, explains the lending motivation and the related higher-ranked lifetime problem. A bound with one separately chosen Fut cannot express every future whose type depends on the lifetime of an argument to each invocation. Async callable bounds can describe that relationship. The RFC is design rationale; its proposal notation and discussion of unstable machinery are not the stable syntax contract of February 2025.

Consider an illustrative sequential importer. Its callback waits for a lookup and adds the result to an accumulator captured outside the callback. The importer awaits each invocation before starting the next. A lending callback matches that arrangement: the borrow belongs to the current asynchronous call, and the surrounding scope remains responsible for the accumulator. This is a proposed design example, not a report of an AL+ implementation.

The interface should make the sequential assumption visible. A caller reading it needs to know when it can use the captured state again. If a future keeps a mutable borrow alive, another conflicting use cannot proceed merely because the callback returned that future. The relevant completion point is the end of the borrow, which may extend across an await.

Capturing ownership and lending are separate choices

Moving values into a closure changes who owns those values. It does not, by itself, answer whether futures produced by repeated calls borrow from the closure. This distinction is easy to lose when treating move as a general remedy for lifetime errors. Review the intended lifetime of the closure and the intended lifetime of each invocation separately.

For a library author, that review starts with the work schedule. Does the library await the callback before continuing? Can it keep more than one invocation alive? Does it store the callable for later? Those questions determine what the caller must supply. A convenient example that performs only one call can conceal restrictions that become important when the library repeats or overlaps calls.

The business consequence is maintenance at the integration boundary. If the library's actual behaviour is sequential, forcing callers into shared ownership and synchronization can make that behaviour harder to see. Conversely, accepting a borrowing callback while expecting independent background work leaves the abstraction fighting its own execution model. Clear contracts reduce the amount of ownership reasoning that every integration must rediscover; that is a design argument, not measured delivery savings.

Spawning changes the recommendation

The strongest reason to retain an owned closure returning a future is that the operation genuinely needs to become independent work. A task that may outlive the caller cannot rely on a borrow of a local accumulator that is about to disappear. The executor's requirements also matter, including whether the spawned future must be transferable between threads.

At that boundary, an explicit owned message or owned input can be easier to understand than a lending interface. A shared handle may be appropriate when sharing is actually required, with its synchronization cost and failure behaviour reviewed openly. Async closures do not remove these decisions. In particular, accepting an async callable does not automatically prove that every future it returns satisfies a spawning API's bounds.

The historical stable feature also should not be extended by copying internal trait methods or associated types from RFC examples into a public API. Use the stable surface supported by the selected toolchain and inspect requirements the callback contract cannot conveniently express. If the existing owned callback is clear and adequate, retaining it is a reasonable decision.

Review the operation that holds the borrow

This subject differs from precise capture rules for return-position impl Trait. Those rules concern which generic parameters an opaque return type may capture. Here the central question is whether a future produced by an invocation can borrow from the callable's captured environment. Both involve lifetimes, but changing an opaque return signature does not settle how an asynchronous callback should be invoked.

For a custom software development review, select one callback that currently causes ownership friction. Describe its owner, the state it borrows and the point where the caller may access that state again. Then describe whether the work is awaited locally or spawned independently. Prefer a lending async callback for the former when borrowing is needed, and an owned handoff for the latter when independence is required. That decision gives the syntax a concrete job and gives the next maintainer an execution model they can inspect.