TLDR: When a server mutation fails, calling mutate() to revalidate your SWR cache isn't optional. Skip it and your UI silently shows the user a state the server already rejected.

the setup

I was deep in a full audit of an internal KOL CRM — a KOL (key opinion leader, my name for the people an ecommerce business brings in to speak on webinars) relationship board built in Next.js.

22 findings in one night. WIN-1 through WIN-7, SEC-1 through SEC-11, all dispositioned same-day.

And the one that bothered me most wasn't the scariest one on the list.

It was WIN-6.

the wall we hit

The board lets you mutate state — move a KOL through a workflow, update a status, record a touchpoint.

SWR (stale-while-revalidate, the React data-fetching library) makes this feel slick. You fire the mutation, the cache updates fast, the UI snaps into the new state.

What I missed: what happens when the mutation fails.

The server rejects the write. Supabase returns an error. And SWR… just… sits there. Cache unchanged. Board still showing the state the server already refused to accept.

No flash. No rollback. No indication anything went wrong.

The screen lied.

what I tried first

I'd already wired up error toasts earlier in the audit. User sees a toast, user knows something failed. Job done, right?

Wrong.

The toast fires. The user reads "Something went wrong." The user looks at the board.

The board still shows the mutation that just failed.

So they don't know if it failed cleanly or if the board is now out of sync. And it IS out of sync — the client thinks one thing, the server knows another.

the fix (WIN-6)

It's one line, but it has to be the right line.

On mutation failure, call mutate() against the affected SWR key.

try {
  await serverAction(payload)
} catch (err) {
  toast.error("Couldn't update — please try again.")
  await mutate(SWR_KEY) // ← pull server truth back into the cache
}

That forced revalidation reaches back to the server and pulls the real state down. Board snaps back to what actually exists. No divergence.

The user sees a toast and the UI self-corrects. That's the full contract.

why it matters to me

The recurring theme from that audit night was silent success theater — things that rendered healthy while something underneath was broken. Same codebase, different file: a supply-chain query silently ignored .error and rendered "Upcoming Webinars: 0" while five were scheduled. Looked fine. Was wrong.

SWR's optimistic local updates are GREAT for feel. But if your error handler only surfaces the error to the user's eyes and doesn't also reconcile the cache with the server, the UI becomes a liar the moment things go sideways.

Toast is for the user. Revalidation is for the data. You need both.