DEV Community

Nicholas Petrasek
Nicholas Petrasek

Posted on AI-assisted

The request timed out. Did the server save the change?

The user clicks Save. A spinner runs for a while, then the request times out.

It is tempting to show “Save failed” and a Retry button. For a write, that may leave out the hardest part: the browser does not necessarily know whether the operation was applied.

This is a design walkthrough for a hypothetical admin action. The same question appears in imports, saved items, and other operations that change server state.

The response is evidence about an operation

Consider three possible sequences:

Server outcome What the browser receives
The request never reaches the handler No successful response
The handler rejects the operation A rejection, if the response arrives
The server commits, then the response is lost No successful response

The first and third can look the same to the client. A transport failure is therefore not enough evidence to claim that nothing changed.

I would represent at least these outcomes in the UI:

  • Confirmed success: the operation was applied.
  • Confirmed rejection: the operation was not accepted under the server's contract.
  • Unknown outcome: the client cannot yet establish what happened.

The third state needs an explanation and a recovery path. It should not pretend to be either of the first two.

Make the attempted operation identifiable

For an operation that must avoid duplicate application, one possible contract uses a client-generated operation identifier.

operation: op-123
action: import the reviewed file
payload: the approved plan
Enter fullscreen mode Exit fullscreen mode

The server associates the identifier with the intended payload and result. A retry using the same identifier can refer to the same logical operation rather than create a new one.

That is a proposed application contract, not behavior supplied automatically by a custom request header. It needs implementation: concurrent retries, payload mismatches, authorization, retention, and failure recovery all matter.

For example, accepting the same identifier with different import contents would make it difficult to know what was retried. The server should define what it accepts and how it reports a mismatch.

Recover the outcome

A status endpoint can let the browser ask about op-123. A fresh read can also work when the application can reliably identify the attempted change. Merely seeing a similar record may be insufficient when concurrent users can create similar data.

During reconciliation, a message such as “We couldn't confirm the import. Checking its status…” describes the available evidence.

If the server is still working, show that state. If it committed, show the result. If the outcome remains unknown, explain which action is safe instead of inventing a result.

Retries need a defined effect

HTTP semantics defines idempotent methods in terms of their intended server effect when identical requests are repeated. That helps when deciding what a client can safely retry, but an operation's application-level behavior still needs attention.

A retried “create another import” command and a retry of one identified import do not express the same intent.

I would choose the retry policy at the same time as the endpoint contract, rather than bolt it onto a generic error handler.

Cancel has its own meaning

Closing a dialog or aborting a fetch stops part of the client interaction. It does not establish that a server-side write was reversed.

A long-running operation may support a cancellation request. That request also has an outcome: the job might stop before applying anything, stop after partial work, or finish before cancellation is accepted.

The UI needs to describe the actual result. “Cancellation requested” is a more limited statement than “Nothing was imported.”

The failure test worth running

Let the server apply the operation, then drop the response. Reload the page and retry the same operation from another tab.

Check whether the application can recover one authoritative result, whether the user understands the state, and whether the retry creates duplicate work.

This is where a reliable workflow separates itself from a demo that only handles a successful response. What does your application show when the write may have succeeded but the browser cannot prove it yet?

Top comments (1)

Collapse
 
arhancanli profile image
Arhan Canli •

The status endpoint has one trap: "not found" for op-123 can mean the write never happened or that the first request is still queued or in flight. If the client takes "not found" as "safe to resubmit", the retry races the original and you get the duplicate you were avoiding.

What closes it is recording the operation id in the same transaction as the write (a unique key on the id). Then the server can answer three ways: applied, in progress, or "never seen, and now blocked": the status check itself inserts a tombstone for that id, so a late-arriving original loses the unique-key race and is rejected. Only after that does "unknown" become a definite "not applied".

Does your sketch keep the id for as long as a proxy or queue can still replay the original request?