Failures and Retries
When a request fails, use-remote-data doesn't throw the error away or bury it in a boolean.
It moves the store to the Failed state — which holds the error and a retry callback that re-runs the exact same request.
How it works
When your Promise rejects (or throws), the store transitions to:
{
type: 'failed',
errors: [...], // what went wrong
retry: () => void // call this to try again
}
The <Await> component renders an error view automatically. The default one shows the error details and a retry button.
When the user clicks retry, the store goes back to Pending and re-fires the request. If it succeeds this time, you get Success. If it fails again, you're back to Failed with a fresh retry.
No useState for tracking error state. No try/catch boilerplate. No manual "retry" logic.
No try/catch. No error boundaries.
With use-remote-data, there is never a reason to write try/catch for data fetching.
Errors don't throw. They don't propagate up the tree. They don't crash your app.
A failed request is just data — the Failed state — handled in the same place you render your success state.
This also means you don't need React error boundaries for data fetching errors.
Error boundaries catch thrown errors during rendering. Since use-remote-data never throws,
the <Await> component always renders cleanly — either your data callback, the pending view, or the error view.
There's nothing to catch.
Retries with combined stores
This is where it gets powerful. When you combine multiple stores with RemoteDataStore.all(),
and one of them fails:
- The combined store moves to
Failed. - The combined
retryonly re-fetches the stores that failed. The successful ones keep their data. - Once the failing store succeeds, the combined store moves to
Successwith the full tuple.
Try it — this example has a store that fails every 10th call, combined with a store that always succeeds. Hit retry a few times to see how only the broken request re-fires.
import {
RefreshStrategy,
RemoteDataStore,
useRemoteData,
Await,
} from 'use-remote-data';
let i = 0;
const freshData = (): Promise<number> =>
new Promise((resolve) => {
i += 1;
setTimeout(() => resolve(i), 1000);
});
let j = 0;
const failSometimes = (): Promise<number> =>
new Promise((resolve, reject) => {
j += 1;
if (j % 10 === 0) reject(`${j} was dividable by 10`);
else resolve(j);
});
export function Component() {
const one = useRemoteData(freshData, {
refresh: RefreshStrategy.afterMillis(1000),
});
const two = useRemoteData(failSometimes, {
refresh: RefreshStrategy.afterMillis(100),
});
return (
<Await store={RemoteDataStore.all(one, two)}>
{([num1, num2]) => (
<span>
{num1} - {num2}
</span>
)}
</Await>
);
}
Customizing the error UI
The built-in error component is deliberately minimal — it's meant to be replaced.
Pass your own error render prop to <Await>:
<Await
store={store}
error={({ errors, retry, storeName }) => (
<div className="error-banner">
<p>Something went wrong{storeName ? ` loading ${storeName}` : ''}.</p>
<button onClick={retry}>Try again</button>
</div>
)}
>
{(data) => <Display data={data} />}
</Await>
The ErrorProps type gives you:
| Prop | Type | What it is |
|---|---|---|
errors | Failure<WeakError, E>[] | The errors that caused the failure (see below) |
retry | () => Promise<void> | Re-runs the failed request |
storeName | string | undefined | The storeName you passed to the hook (useful for debugging) |
For most apps, you only need retry — the errors array is there when you want to show specifics.
A note on the errors type
errors is an array because combined stores can have multiple failures (one per failed request). For a single store, it's always a one-element array.
Each element is a Failure<WeakError, E> — a tagged object that is one of two things:
{ tag: 'unexpected', value: WeakError }— an unexpected error (network failure, thrown exception, etc.).WeakErroris just an alias forError | unknown, because JavaScript lets youthrowanything.{ tag: 'expected', value: E }— a typed domain error you returned explicitly (only when using the typed errors feature).
If you're not using typed errors, every error will be unexpected — so you can treat failure.value as the thrown value. Most of the time you won't inspect this at all; the retry callback is enough.
Mutations fail the same way
useRemoteUpdate uses the same error model.
If a mutation fails, the store moves to Failed, and the default <AwaitUpdate> error view shows a retry button that re-runs with the same parameters that were originally passed.
const saveStore = useRemoteUpdate((name: string) => api.save(name));
// If save("Alice") fails, retry() calls save("Alice") again
saveStore.run('Alice');
You can also call reset() to go back to Initial instead of retrying — useful when you'd rather let the user fix their input and try again.