SkillByAIOpen interactive version →

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.