Expired auth token throws into React 18 Suspense ErrorBoundary with no cache invalidation or retry API
0 reputation · 24 Mar 2022, 08:12 UTC
0 reputation · 24 Mar 2022, 08:12 UTC
React 18 documents Suspense-enabled data fetching as experimental. When a fetch of a protected resource rejects because an authentication token has expired, the thrown error propagates to the nearest ErrorBoundary, and Suspense itself provides no retry or fallback mechanism.
The Suspense cache is keyed by the data fetching function, and the public API exposes no method to manually invalidate or refresh a cached entry. Suspense does not distinguish transient failures such as token expiry from permanent errors, and no built-in token refresh flow exists, so authentication recovery is left entirely to custom wrapper logic.
The design goal is a protected-data view that can recover from an expired credential without unmounting the whole tree or serving stale cached data after the token is renewed.
How should token expiry be modeled inside an ErrorBoundary so that a refresh-and-refetch cycle is possible? What invalidation strategy fits a cache that exposes no manual invalidation API? Should a library such as @tanstack/react-query own this flow instead of core Suspense?
34775 reputation · 24 Mar 2022, 12:33 UTC
When a protected fetch fails with a 401/403 because the bearer token has expired, React 18 Suspense simply throws that error to the nearest ErrorBoundary. Suspense itself does not know that the failure is transient, nor does it expose a public API to clear or refresh a cached entry. The only way to recover is to let the ErrorBoundary perform a token‑refresh, then force Suspense to re‑execute the data‑fetching thunk.
throw new AuthExpiredError('Token expired');. This separates recoverable auth errors from permanent ones.ErrorBoundary, implement componentDidCatch(error) and check error instanceof AuthExpiredError. In a functional boundary, use useErrorBoundary or the onReset pattern.Suspense caches by the thunk’s identity and its arguments. There is no API to delete a specific entry, so you must change the key that the thunk uses:
tokenVersion or timestamp argument to the thunk. Store the current version in context; increment it after a successful refresh. The thunk signature becomes const fetchData = (v) => fetch('/data', {headers:{Authorization:`Bearer ${getToken(v)}`}});. When tokenVersion changes, Suspense treats it as a new key and starts a fresh fetch.key is tied to the token version. Changing the key unmounts the subtree, clearing Suspense’s internal cache for that branch. This is heavier but guarantees a clean slate.For production‑grade authentication, @tanstack/react-query is a better fit. It provides explicit query keys, manual invalidation (queryClient.invalidateQueries), and retry hooks. A typical pattern is:
const queryFn = async () => {
try { return await fetchWithAuth(); }
catch (e) {
if (isTokenExpired(e)) {
await refreshToken();
return await fetchWithAuth();
}
throw e;
}
};
useQuery(['data', tokenVersion], queryFn, { suspense: true });
React‑Query will automatically refetch after tokenVersion changes and will not serve stale data.
Do you currently use a class ErrorBoundary with componentDidCatch or the React 19 onReset hook? The exact pattern determines how you trigger the re‑render that re‑executes the thunk.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.