FREE LESSON · Applications & software architecture · 11 OF 11
Asynchronous interfaces and stale responses
Asynchronous interfaces and stale responses: Model a user interaction as states and make response ownership explicit.
Prerequisites: Functions, state machines, and asynchronous requests.
Learning objectives
- Represent loading, success, and failure as explicit states.
- Prevent an old response from replacing a newer result.
- Separate cancellation of interest from cancellation of effects.
A user interface is a concurrent program
A search interface receives input events, starts requests, and renders results. Responses can arrive in a different order from requests. If every response blindly replaces the screen, a slow old query can overwrite a newer one. The bug is a state-ownership problem, not simply a slow network.
Suppose the user types cat and then caterpillar. Request A searches cat; request B searches caterpillar. B completes first, and the screen displays the intended results. A completes later. An implementation that accepts every success now shows cat results under a caterpillar input. The displayed state has combined evidence from two different operations.
Give each operation an identity
Maintain a monotonically increasing request generation. When a request starts, capture its generation and query. When it finishes, update the visible result only if its generation still matches the current one. The same guard must apply to errors and loading flags. Otherwise an old failed request can replace a new successful result with an error, or an old completion can hide a loading indicator for work still in progress.
on query change:
current_generation += 1
g = current_generation
state = Loading(query)
begin request(query)
on success(g, query, results):
if g == current_generation:
state = Ready(query, results)
on failure(g, query, error):
if g == current_generation:
state = Failed(query, error)
This is pseudocode. It describes ownership of the visible result, not the details of a particular UI framework. If multiple independent panels can load concurrently, each needs an appropriately scoped identity rather than one global counter that causes unrelated requests to suppress each other.
Cancellation has several meanings
Aborting a browser request can save work and indicate that the caller no longer wants a result. It does not prove that a server-side effect never happened. A cancelled search may be harmless; a cancelled purchase request can still have committed. UI cancellation, transport interruption, and business cancellation are separate operations with different contracts.
Represent the state as a tagged alternative such as Idle, Loading(query), Ready(query,results), or Failed(query,error). Independent booleans can admit contradictory combinations, such as both success and loading with no clear displayed query. Preserve previous results while loading only if that behavior is intentional and clearly attributed to the previous query.
Test the schedule, not just the final happy path: resolve B before A, fail A after B succeeds, change input while a request is pending, and leave the page before a response arrives. A deterministic fake request controller makes these schedules reproducible. The mechanism generalizes to autosave, validation, and any interface where old work can arrive after new intent.
Practice problem
Trace requests A and B through the generation scheme when B succeeds before A fails. Then explain why the same UI guard is insufficient to prevent duplicate writes in an autosave endpoint.
Worked solution
A captures generation 1 and B captures generation 2. B’s success matches the current generation and installs its results. A’s later failure has generation 1 and is ignored for the current screen. For autosave, ignoring an old response does not stop an old request from overwriting newer server state. The server needs a concurrency contract, such as a version precondition or serialized updates, and the client must handle conflicts. UI freshness and durable-write ordering are different invariants.
Check your understanding
An old request fails after the latest request succeeds. Which state should it change?
- The current success must become an error.
- The current loading flag only.
- None of the current query’s visible state.
- The current query text.
Explanation
The old operation no longer owns the active query state.
Does aborting a request prove that its server-side write did not commit?
- Yes, on every transport.
- Only when the browser hides the result.
- Yes, if the UI shows a spinner.
- No; the write outcome needs its own protocol.
Explanation
An interrupted response leaves the operation outcome uncertain unless the server’s contract provides stronger evidence.
Primary reading
- Software Engineering at Google: Google, open book
- How to Design Programs: Felleisen et al.