Lesson 8 / 25
Loading and Error State
Typed errors, unwrap and avoiding duplicate requests.
Handling failures cleanly
Model request state as a status union ('idle' | 'loading' | 'succeeded' | 'failed') rather than several booleans that can contradict each other. Use rejectWithValue(value) to return a typed, serialisable error payload (for example a server validation message) instead of a thrown Error. In a component, await dispatch(thunk(arg)).unwrap() returns the fulfilled payload or throws, which is handy for showing a toast after submit. The condition option can cancel a thunk before it starts, for example when a fetch is already in flight.
rejectWithValue, condition and unwrap
A save thunk with typed errors.
type ApiError = { message: string };
export const saveProfile = createAsyncThunk<
User, // fulfilled payload
User, // argument
{ state: RootState; rejectValue: ApiError }
>(
'user/save',
async (user, { rejectWithValue }) => {
const res = await fetch('/api/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user),
});
if (!res.ok) return rejectWithValue({ message: `Save failed (${res.status})` });
return (await res.json()) as User;
},
{ condition: (_user, { getState }) => getState().user.status !== 'loading' }
);
// in a component
async function onSubmit(user: User) {
try {
await dispatch(saveProfile(user)).unwrap();
toast('Saved');
} catch (err) {
toast((err as ApiError).message);
}
}Errors in state must be serialisable
Store an error message or code string, not an Error object; the serialisability check will warn about class instances in state.
Quick check: What does .unwrap() on a dispatched thunk result do?
- Cancels the request
- Removes the thunk from the store
- Returns the fulfilled payload or throws the rejection
- Converts the thunk into a slice reducer
Answer
Returns the fulfilled payload or throws the rejection — unwrap lets components use try/catch around a thunk.