DEV Community

137Foundry
137Foundry

Posted on

How to Add Optimistic UI Updates Without Lying to Users When a Save Fails

Optimistic UI updates make an app feel instant: the interface reflects a change immediately, before the server confirms it. Done well, this is the difference between an app that feels responsive and one that feels sluggish behind every save button. Done carelessly, it means the UI shows a state that turns out to be false, which erodes trust faster than a slightly slower, more honest interface would have.

Step 1: Update Local State Immediately

The core of optimistic UI: when a user performs an action, like sending a message or liking a post, update the local UI state synchronously, before any network request completes. The user sees the result of their action instantly. The network request to persist that change happens in the background afterward.

Step 2: Keep a Reference to the Pre-Update State

Before applying the optimistic update, snapshot the state you're about to change. If the server request later fails, you need to roll back to exactly this prior state, not an approximation of it. Skipping this step is how optimistic UI implementations end up with subtle bugs where a failed rollback leaves the interface in a state that doesn't match either the old or the new reality.

Step 3: Handle the Failure Path as Carefully as the Success Path

It's tempting to build the optimistic update path thoroughly and treat the failure path as an afterthought, since failures are the less common case during development and testing. In production, failures happen constantly, expired sessions, validation errors, network drops, and a rollback path that wasn't tested as carefully as the happy path is where users start noticing the UI showing something that isn't actually true.

Step 4: Show the Rollback, Don't Just Silently Revert

When an optimistic update fails and you roll back, don't just silently snap the UI back to the old state with no explanation. A brief, clear message, "Message failed to send, tap to retry," gives the user agency to fix the problem, rather than leaving them wondering why their action seemingly undid itself with no visible cause. Nielsen Norman Group's error message guidelines are a solid reference for writing this kind of message clearly and without unnecessary technical jargon.

Step 5: Distinguish Between Retryable and Non-Retryable Failures

Not every failure should be presented the same way. A transient network failure is worth an automatic retry with a visible pending state, since the action will likely succeed shortly. A validation failure, like a comment that violates a length limit, needs the user to change something before retrying makes sense. Presenting both failure types identically confuses users about what action, if any, they need to take.

Step 6: Be Careful With Optimistic Updates That Affect Other Users

A like count, a comment count, a shared document edit, anything visible to other users carries more risk when done optimistically, because a rollback after other users have already seen the optimistic state creates a more visible inconsistency than a rollback that only the acting user witnessed. For these cases, consider a shorter optimistic window or a more conservative confirmation-based approach, since the cost of a visible rollback scales with how many people saw the incorrect state.

Step 7: Test the Rollback Path Explicitly, Not Just the Happy Path

Automated tests for optimistic UI features often only assert on the success case, since that's the primary user story being implemented. Writing explicit tests that simulate a server failure and assert the UI correctly rolls back to the exact pre-update state catches a category of bugs that's otherwise easy to ship unnoticed, since the failure path might work fine in a quick manual check but break under a specific timing or state combination that only a dedicated test catches. Kent C. Dodds' testing guidance covers testing async and error-state UI behavior in detail, applicable well beyond the specific framework examples used.

Step 8: Combine Optimistic UI With the Same Sync Architecture as Offline Support

Optimistic UI and offline-first sync solve related problems with overlapping mechanics: both need a local state that can diverge from server state temporarily, and both need a reliable reconciliation path when server confirmation arrives, whether that's seconds later on a good connection or hours later after a device reconnects. Building these on the same underlying local-write-and-queue architecture, rather than as two separate systems, avoids duplicating the rollback and reconciliation logic in two different places. There's a full breakdown of that underlying architecture in How to Build Offline-First Data Sync for a Mobile App Without Losing Local Edits.

Step 9: Watch Real User Sessions to Catch Rollback UX That Feels Bad in Practice

Session replay tools and analytics on how often rollbacks happen in production surface UX problems that don't show up in a design review. If a specific action rolls back unusually often, that's a signal worth investigating, whether it's a backend validation issue, a race condition, or a UI element that's too easy to trigger accidentally. Nielsen Norman Group has written extensively on system status visibility as a core usability heuristic, which is the underlying principle optimistic UI has to respect even while prioritizing perceived speed.

Step 10: Give Optimistic Actions a Distinct Visual Treatment While Pending

A subtle visual difference, slightly reduced opacity, a small pending indicator, between a confirmed state and an optimistically-applied-but-unconfirmed state gives sophisticated users a way to distinguish the two without requiring an explicit status message for every single action. This is a lighter-weight signal than a full sync indicator and works well for fast, frequent actions like likes or reactions where a persistent status label would be visual overkill.

Step 11: Queue Optimistic Actions in Order, Not in Parallel

If a user performs several optimistic actions in quick succession on the same resource, applying the corresponding server requests in parallel can result in them completing out of order, producing a final server state that doesn't match the order the user actually performed the actions in. Processing queued mutations against the same resource sequentially, rather than firing them all in parallel, avoids this class of ordering bug, at some cost to raw throughput that's almost always worth paying for the correctness guarantee.

Step 12: Document the Optimistic Behavior for Your Own Team

Optimistic UI logic tends to be some of the least obvious code in a codebase to a developer encountering it for the first time, since the UI and the actual server state can legitimately diverge for a period by design, which looks like a bug to someone unfamiliar with the pattern. Clear code comments and documentation explaining which actions are optimistic and why saves future debugging time when someone new investigates what looks like inconsistent behavior but is actually working as intended.

137Foundry builds optimistic UI and offline-sync architecture together as a single coherent system for clients who need both, rather than bolting them on separately after the fact, since the two share enough underlying mechanics that treating them as one system produces fewer edge cases than building them independently. More on the team's approach is at 137foundry.com, including how this pattern applies specifically to mobile clients with unreliable connectivity.

Top comments (0)