React Query: why a failed request showed $0.00 on my dashboard
When a query fails, isPending goes false and data stays undefined. A component that only reads those two shows its default values as if they were real. How a Playwright test caught it, and the fix.
A dashboard I built had three cards at the top: open commitment, overdue orders and orders completed this month. A manager checks them every morning.
Then I wrote a test that kills the request behind them on purpose. The cards did not show an error. They showed this:
$0.00 · 0 orders · 0 orders
No red, no warning. Just clean zeros where real numbers should be. A manager would read that as "no money committed" and move on.
Why it happened
The component read two things from useQuery: data and isPending.
const { data, isPending } = useQuery({ queryKey, queryFn });
let openCommitment = 0;
if (data && !isPending) {
openCommitment = data.reduce((sum, po) => sum + po.total_amount, 0);
}
return (
<StatCard
label="Open commitment"
value={isPending ? "..." : `$${openCommitment.toFixed(2)}`}
/>
);This looks fine. It handles loading, and it handles data. But a query has a third state.
When a query fails, React Query sets isPending to false and leaves data as undefined. So:
isPendingis false, so the card stops showing "..."datais undefined, so the calculation is skippedopenCommitmentkeeps the0it started with- the card prints
$0.00
The code only knew two states: loading and done. "Failed" fell through to "done, and the total is zero".
The fix
Read isError too, and never let a starting value pretend to be data.
const { data, isPending, isError } = useQuery({
queryKey,
queryFn: async () => {
const result = await getMetrics();
// Make the query actually fail when the server says it failed.
if (!result.success) throw new Error("Failed to load metrics");
return result.data;
},
});
// "…" while loading, "—" when the query failed, never a fake zero.
const display = (format: (n: number) => string, raw?: number) => {
if (isPending) return "…";
if (isError || raw === undefined) return "—";
return format(raw);
};Two changes matter here.
First, the card has three outcomes now: loading, failed and loaded. A dash says "I don't know". A zero says "there is nothing". Those mean very different things to the person reading them.
Second, the queryFn throws when the server returns success: false. If your fetcher turns errors into an empty result, React Query never knows the request failed, and isError stays false forever.

How it was caught
Not by clicking around. On my system the request always worked, so I never saw it.
A Playwright test caught it. The test kills the request on purpose and checks that no card shows a number:
test("metric cards show no figure when the data fails to load", async ({ page }) => {
await onServerActions(page, (route) => route.abort("failed"));
await page.goto("/purchase-orders");
// React Query retries 3 times (about 7 s) before it reports an error.
await expect(openCommitment).toContainText("—", { timeout: 15_000 });
// A failed load must never look like real zeros.
await expect(openCommitment).not.toContainText("$0.00");
});The data loads through a Next.js Server Action, so the request to block is a POST to the page URL with a next-action header, not a call to /rest/v1/.
Check your own components
Search for components that read isPending or isLoading but never isError:
grep -rlE "isPending|isLoading" src | xargs grep -L "isError"Every file in that list has a question to answer: what does this screen show when the request fails? If the answer is "a zero" or "no results", that's a bug that just hasn't been reported yet.