An asynchronous operation can start first and finish last. If every completion writes into the same state slot, completion order becomes state order even when the application intended request order to define authority.

This race appears without shared-memory threads. Two network requests, background computations, database queries, or worker messages can overlap through an event loop and return in the opposite order from their initiation. The older result is not necessarily incorrect data. It is stale because a later request has superseded the state transition that originally authorized it.

A generation counter makes that supersession explicit. Each new operation receives a monotonically increasing generation. A completion may publish its result only while its generation is still current. The mechanism separates finishing work from retaining authority to change state.

Completion order is not request order

Consider a search interface that starts request 17 for one query, then starts request 18 after the query changes. Request 18 completes quickly and its result becomes visible. Request 17 then arrives late.

An unconditional completion handler produces this sequence:

start 17
start 18
finish 18 -> publish 18
finish 17 -> publish 17

The final state corresponds to request 17 even though request 18 represents the later intent. Nothing in asynchronous execution requires operations to finish in initiation order. Latency, scheduling, cache state, server load, and downstream work can all affect completion independently.

The race is therefore a state-admission problem rather than merely a timing problem. Waiting for request 17 to finish before starting 18 would impose serialization, but serialization is not required if overlapping work is desirable. The state holder instead needs a rule for deciding which completion still has permission to publish.

A counter turns supersession into a comparison

A minimal model keeps one counter beside the state:

current_generation = 0

start():
    current_generation += 1
    mine = current_generation
    begin_async_work(mine)

complete(mine, value):
    if mine == current_generation:
        state = value

Request 17 captures generation 17. Starting request 18 advances the current generation to 18. When 17 eventually completes, its comparison fails and its result is discarded. Request 18 can publish while 18 remains current.

The counter does not predict completion order and does not force an operation to stop. It changes the admission condition at the point where an asynchronous result becomes shared application state.

A Boolean such as is_loading cannot encode the same ordering. It can indicate that some work is active, but it cannot distinguish an older completion from the current operation once multiple requests overlap. A unique random identifier can distinguish requests, but a counter also expresses a succession relation that is useful when later generations supersede earlier ones.

Cancellation and stale-result rejection solve different problems

Cancellation can reduce work after an operation loses relevance. It does not automatically provide the state-safety property supplied by a generation check.

A cancellation request can race with completion. Some APIs cannot interrupt work after it has crossed a particular boundary. Remote services may continue processing after a local client stops waiting. A callback can already be queued when cancellation is requested.

Generation comparison remains useful in those cases because it is evaluated when the result attempts to affect current state. Cancellation says that obsolete work should stop when the execution model permits it. Generation admission says that obsolete work must not publish after a newer generation has taken authority.

The two mechanisms can therefore coexist. Advancing the generation can invalidate old completions immediately, while cancellation is sent as a resource-management action. Correctness does not need to depend on cancellation winning every race.

The protected state defines the counter’s scope

A single global counter is correct only when every new operation supersedes every earlier operation for the same state domain.

Suppose a screen loads an account summary and an activity feed independently. If starting a feed request increments the same generation used by the summary request, a valid summary completion can be rejected even though the two results do not compete for the same state.

Separate state domains call for separate generations:

summary_generation
feed_generation

The same issue appears with keyed data. Requests for customer A and customer B may proceed independently if their results occupy independent entries. A generation per key can express that separation, while one process-wide counter would create false conflicts.

Counter placement is therefore part of the concurrency model. The counter belongs with the state whose authority is being superseded, not automatically with the component, process, or transport that happens to launch the work.

Equality encodes a latest-request policy

The predicate mine == current_generation expresses a specific policy: only the most recently started generation may publish.

That policy fits replacement state, such as search results for the current query. It does not fit every asynchronous workflow.

An append-only event stream may need to retain every result and reorder them by sequence. A batch aggregator may accept results from several generations until a batch closes. A cache can permit an older fetch to populate an entry when no newer value has committed, depending on its consistency contract. In those models, rejecting every non-current generation would discard legitimate work.

The comparison must match the state transition semantics. A counter supplies ordering information; the admission rule decides what that ordering means.

This distinction also matters when operations have multiple completion phases. If generation 24 publishes partial state and generation 25 begins, a later phase from 24 should normally fail the same authority check if 25 supersedes the whole operation. If phases own independent state, each state boundary may require its own admission rule.

Counter lifetime must match state lifetime

A generation has meaning only relative to the counter that issued it. Resetting the counter while old operations can still complete can accidentally make a stale generation current again.

Consider a component that starts generation 1, is reset to zero, then starts a new generation 1 while the original operation remains in flight. The old completion now carries a value equal to the current counter. The equality check no longer distinguishes the two lifetimes.

Several designs avoid that aliasing. The counter can live as long as any issued operation can return. A recreated state owner can use a fresh identity alongside its local counter. A sufficiently wide monotonic integer can continue across resets that do not represent a new authority domain.

Integer wraparound is the same issue at a longer timescale. If the implementation type can wrap while old generations remain possible, equality can eventually alias. The practical condition is not that a counter must be mathematically unbounded; its value space and reset rules must prevent reuse during the lifetime of any completion that could still reach the admission point.

The comparison must guard the actual mutation

Checking a generation and then deferring the state write can reopen a race if another request can advance the generation between those actions.

This pattern has a gap:

if mine == current_generation:
    schedule_state_write(value)

If schedule_state_write runs later, a newer generation can start before the scheduled mutation executes. The earlier completion passed its check while current but publishes after losing authority.

The check and protected state transition need ordering semantics that exclude that gap. In a single event-loop turn, direct comparison followed by mutation may already have the required indivisibility relative to other callbacks. In multithreaded code, synchronization or an atomic state primitive may be required. Framework-specific scheduling can add another queue boundary that has to be included in the reasoning.

The relevant invariant is precise: a result from generation N must not commit to the protected state after a later generation has superseded N under the application’s ordering model.

Authority belongs at the state boundary

Out-of-order completion is unavoidable in many concurrent systems; stale publication is not. A generation counter converts temporal intent into data that the state holder can compare.

Its value is narrow. It does not cancel obsolete work, make requests idempotent, serialize independent operations, or establish ordering across unrelated state domains. It records which generation currently owns permission to replace a particular piece of state.

That boundary makes the mechanism inspectable. An asynchronous result can finish successfully and still be denied a state transition because success of the work and authority to publish are separate facts. Once those facts are represented separately, completion order no longer has to decide which result survives.