Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-254 · Case file with fixtures · Case 226 of 694 · Runtime evidence

Why Result::and Evaluates the Second Operation After an Error

and receives an already-evaluated Result, so Rust performs the second operation before preserving the first error. Pass a closure to and_then when execution itself must short-circuit.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
The method receives an already-evaluated Result value, so ordinary argument evaluation happens before variant selection.
First discriminating check
Count second-operation calls for an initial Err and compare passing the result value to and with a closure to and_then.

This expression looks like a fallible sequence:

first().and(second())

The returned Result short-circuits, but evaluation of the expressions does not. second() runs before and can inspect the first result.

The failing program starts with Err("first failed") and counts calls to the second operation. Result::and preserves the first error, yet the count becomes one.

and combines values already produced

The method receives self and another Result<U, E>. Rust evaluates the second call to obtain that argument. Only then can and return the second result for Ok or preserve the first error for Err.

This is different from language-level &&, whose right expression is conditionally evaluated. A familiar name does not override ordinary call evaluation.

and is useful when both results already exist and I only need to combine them. It is not a lazy sequencing primitive.

and_then receives deferred work

The repaired program passes the second operation as a closure/function to and_then. The function runs only when the first value is Ok, and it receives that success value.

This makes data dependency and execution dependency agree:

first().and_then(second)

The returned error and the performed side effects now short-circuit together.

A correct final error can hide incorrect work

Tests that assert only Err("first failed") pass for both versions. They do not reveal that second() sent a request, wrote a file, incremented a counter, or consumed input.

The Atlas fixture counts calls rather than timing them. In application tests I use a fake dependency recording exact invocations. Outcome assertions and trajectory assertions protect different properties.

This matters most for irreversible actions. A preserved first error does not undo a payment attempt or an external message created while evaluating the argument.

Independent validation can intentionally be eager

Sometimes I want to run several validations to collect all errors rather than stop at the first. Result::and still does not collect errors automatically; it follows its documented result table and can discard one outcome.

For error accumulation I use a validation structure designed to retain multiple failures. For sequential dependency I use and_then or ?. I avoid using eager evaluation accidentally as a substitute for an explicit parallel or aggregate policy.

Async code makes the boundary more visible

Creating a future is not always the same as executing it, but an async function call can still evaluate arguments and construct state. Awaiting or spawning determines when the operation runs.

first.await.and(second.await) necessarily awaits the second before calling and, even when the first result is an error. The usual sequential form is:

let value = first().await?;
let next = second(value).await?;

If operations are independent and should overlap, that concurrency choice deserves an explicit join and error policy.

Error type equality is part of and

and can change the success type from T to U, but both results use the same error type E. and_then has the same error constraint.

When layers produce different errors, I map them into a meaningful domain error rather than erasing details only to make a combinator type-check. Good sequencing keeps both execution and diagnostics legible.

Result::or also receives an eager result, while or_else defers fallback work. map_or receives an eager default, while map_or_else receives closures. The repeated review question is simple: am I passing completed work or a callable description of work?

I still use the eager forms for constants and already-computed values. Laziness is not automatically better; it is necessary when work must be conditional.

What I test

The regression covers first Ok and first Err, counts second calls, and asserts which error survives when both operations fail. For side-effecting code it checks the recorded action log, not just the returned variant.

Construction can be observable before the function body

Even if the second function immediately returns a constant Result, evaluating its arguments may clone data, borrow locks, allocate buffers, or move values. Deferring only an inner network call while eagerly constructing all of its inputs can still create unwanted work.

With and_then, captures are created when the closure is built, but the body is conditional. I inspect which expressions live outside and inside the closure. Moving an expensive preparation inside is often required for true laziness; moving ownership into the closure is still cheap but changes later availability.

This makes the execution boundary visible enough to review instead of assuming the combinator moves it automatically.

The core principle is that value-level short-circuiting does not imply evaluation-level short-circuiting. Result::and selects between two Result values after both expressions exist. and_then controls whether the second operation executes. Choosing between them requires looking at work, not only return types.