DEV Community

137Foundry
137Foundry

Posted on

How to Add a Rollback Toast When an Optimistic Update Fails

An optimistic update that fails silently is worse than no optimistic update at all. The user saw their change take effect, then watched it quietly disappear with no explanation. A rollback toast fixes that in about an hour of work, and it's one of the highest-leverage additions you can make to an existing optimistic UI implementation.

An open notebook with pages of handwritten annotated diagrams on a desk
Photo by Pavel Danilyuk on Pexels

Step 1: Centralize Your Rollback Logic

Before adding a toast, make sure every optimistic mutation in your app funnels through a single rollback function, rather than each feature handling its own undo logic inline. If you're using TanStack Query, this usually lives in the onError callback of your mutation, paired with the context object returned from onMutate that captured the previous state.

useMutation({
  mutationFn: updateTask,
  onMutate: async (newTask) => {
    await queryClient.cancelQueries(['tasks']);
    const previous = queryClient.getQueryData(['tasks']);
    queryClient.setQueryData(['tasks'], (old) => applyOptimistic(old, newTask));
    return { previous };
  },
  onError: (err, newTask, context) => {
    queryClient.setQueryData(['tasks'], context.previous);
    showRollbackToast(err, newTask);
  },
});
Enter fullscreen mode Exit fullscreen mode

Centralizing this means your toast logic only has to be written once, and every future optimistic mutation gets it for free.

Step 2: Write a Toast Message That Explains What Happened

"Something went wrong" is technically accurate and completely useless. A good rollback toast names the action that failed and, when you can determine it, the reason. "Couldn't move your card, connection issue" tells the user exactly what to expect and whether retrying makes sense.

Keep the message short enough to read in the two or three seconds a toast is typically visible. If the underlying error has more detail worth surfacing, put it behind an expandable "details" link rather than cramming it into the toast body itself.

Step 3: Add a Retry Action Directly in the Toast

For transient failures, a plain rollback message forces the user to redo the entire action from scratch. Adding a retry button directly in the toast, wired back to the same mutation function, turns a dead end into a single click.

function showRollbackToast(err, newTask) {
  toast.error(`Couldn't save "${newTask.title}"`, {
    action: {
      label: 'Retry',
      onClick: () => mutate(newTask),
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

This is a small addition, but it's the difference between a rollback that feels like a minor hiccup and one that feels like the app just ate the user's work.

Step 4: Test the Failure Path on Purpose

Force a rejection at the network layer during development, either through your mock server or a browser dev tools throttling profile that simulates a dropped connection, and confirm three things: the UI actually reverts, the toast appears with an accurate message, and the retry button, if present, successfully resubmits the original action. Tools like Playwright make it straightforward to script this as an automated test rather than relying on manually forcing failures every release.

Step 5: Watch for Toast Pile-Up

If a user's connection drops entirely and several optimistic actions fail in quick succession, make sure your toast system deduplicates or groups similar failures instead of stacking five identical "connection issue" toasts on top of each other. A grouped message like "3 changes couldn't be saved" with a single retry-all action reads far better than a wall of individual toasts.

Step 6: Don't Hand-Roll the Toast Component Itself

The rollback logic is worth writing yourself. The toast rendering, positioning, stacking, and dismiss timers are a solved problem, and a library like react-hot-toast handles the fiddly parts, screen-reader announcements, stacking order, exit animations, that are easy to get subtly wrong on a first attempt. Wire your rollback function to call the library's API rather than building a custom toast component from scratch.

Step 7: Make Sure Toasts Survive a Route Change

If a mutation fails right as the user navigates to a different page, a toast anchored to the old page's component tree can disappear before the user ever sees it. Mount your toast container at the top level of your app, outside any route-specific layout, so a rollback message triggered mid-navigation still reaches the user on whatever screen they land on next.

Step 8: Make the Toast Accessible, Not Just Visible

A toast that only communicates through color and a brief visual appearance excludes anyone using a screen reader. Use an aria-live="polite" region so assistive technology announces the rollback message without interrupting whatever the user was already doing, and make sure the retry button is reachable by keyboard, not just by mouse. The W3C Web Accessibility Initiative has detailed guidance on live regions that's worth following exactly rather than approximating.

Step 9: Handle the Case Where the Toast Itself Fails to Render

It sounds unlikely until it happens: a toast library that hasn't finished initializing, or a portal target that hasn't mounted yet, can silently swallow a rollback notification at the exact moment it matters most, right after the app first loads and a user immediately clicks something. Wrap your toast call in a fallback path, even something as basic as a console.error plus a check that the toast container exists, so a rollback never fails completely silently just because the notification layer itself wasn't ready.

Step 10: Keep the Toast Copy Consistent Across Every Mutation

As a codebase grows, different developers tend to write rollback messages in slightly different voices, one says "Failed to save," another says "Couldn't update," a third writes a full sentence with different punctuation conventions. This is a small thing individually, but it adds up to an app that feels inconsistent in exactly the moments a user is already slightly frustrated. Centralize your rollback message templates the same way you centralized the rollback logic itself in step one, so every failure across the app reads with the same voice and structure.

Putting all ten of these together doesn't require a rewrite. Most teams can retrofit steps one through five onto an existing optimistic UI implementation in a single sprint, then layer in accessibility and consistency improvements as a smaller follow-up pass.

Where This Fits Alongside the Rest of Your Error Handling

A rollback toast shouldn't be your app's only error surface. It handles the specific case of an optimistic mutation failing after the UI already showed success, but plenty of other failures, a page that fails to load entirely, an authentication error that requires a redirect, deserve their own distinct treatment rather than being routed through the same toast system by default. Keep the rollback toast scoped to what it's actually good at: a brief, specific notice about one action that didn't stick, not a general-purpose error reporting mechanism for the whole app.

A Short Checklist Before You Call This Done

Before considering the rollback toast finished, confirm each of these: the rollback function is centralized rather than duplicated per feature, the message names the specific action and, where possible, the reason, a retry action exists for transient failures, toasts group instead of piling up during a connectivity drop, the toast library handles accessibility rather than a hand-rolled component, the container survives route changes, and the copy reads consistently across every mutation in the app. Most of these are small individually, but skipping several of them at once is exactly what turns a rollback into the kind of quiet, confusing failure that generates support tickets instead of a shrug and a retry click.

Adding this whole flow typically takes less time than debugging a single confusing bug report from a user who couldn't tell whether their change actually saved. If you want a deeper look at the state modeling that makes rollbacks reliable in the first place, 137Foundry's longer guide, How to Design Optimistic UI Updates That Roll Back Gracefully, covers the shadow-state pattern this toast logic depends on.

Top comments (0)