This episode teaches createAsyncThunk for fetching API data and storing the results in a reducer. You'll understand the pending, fulfilled, and rejected action lifecycle, manage loading, succeeded, and failed statuses, handle error messages, and abort requests when a component unmounts.

Everything we've done so far is synchronous. Real applications always deal with data from the server: fetching users, posting forms, uploading files — all of it async. Redux Toolkit provides createAsyncThunk to handle this cleanly.
Episode 6 covers how to create an async thunk, the three automatically generated lifecycle actions, the loading, succeeded, and failed status pattern, error handling, and the abort mechanism when a component is no longer active.
createAsyncThunk(typePrefix, payloadCreator) accepts two arguments: an action name prefix and an async function that returns the result.
import { createAsyncThunk } from "@reduxjs/toolkit"
export const fetchPosts = createAsyncThunk("posts/fetchPosts", async () => {
const res = await fetch("/api/posts")
const data = await res.json()
return data
})"posts/fetchPosts" is the prefix; from it, RTK generates three action types: posts/fetchPosts/pending, posts/fetchPosts/fulfilled, and posts/fetchPosts/rejected. createAsyncThunk creates a thunk you can dispatch like a normal action.
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchPosts())
}, [dispatch])dispatch(fetchPosts()) runs the payloadCreator, then dispatches a pending, fulfilled, or rejected action depending on the outcome — without handling any promises manually in the component.
RTK dispatches three actions in sequence:
dispatch(thunk) -> pending -> (success) -> fulfilled
-> (error) -> rejectedaction.payload.action.error.Note that createAsyncThunk only throws an error if the payloadCreator throws an exception or its promise rejects. Errors from HTTP responses like a status 500 don't automatically reject — the response still resolves as long as the fetch succeeded.
The recommended standard pattern is to store status and error in the slice state, then handle them in extraReducers:
import { createSlice } from "@reduxjs/toolkit"
import { fetchPosts } from "./fetchPosts"
const postsSlice = createSlice({
name: "posts",
initialState: { items: [], status: "idle", error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.status = "loading"
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = "succeeded"
state.items = action.payload
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = "failed"
state.error = action.error.message
})
},
})fetchPosts.pending, .fulfilled, and .rejected are generated action creators — usable directly in builder.addCase. state.error = action.error.message stores the error message for display.
const { items, status, error } = useSelector((state) => state.posts)
if (status === "loading") return <p>Loading...</p>
if (status === "failed") return <p>Error: {error}</p>
return <ul>{items.map((p) => <li key={p.id}>{p.title}</li>)}</ul>The status === "loading", then "failed", then rendering data pattern is the most common async UI cycle. The status stays in the store so it can be shared across components.
For business-side errors — such as a 400 validation from the server — use rejectWithValue so a custom message reaches the reducer:
export const fetchPosts = createAsyncThunk(
"posts/fetchPosts",
async (_, { rejectWithValue }) => {
const res = await fetch("/api/posts")
if (!res.ok) {
return rejectWithValue("Failed to load data")
}
return res.json()
},
)rejectWithValue dispatches a rejected action with a payload message you can read as action.payload in the reducer. createAsyncThunk provides a second argument as an object with rejectWithValue, getState, and dispatch.
The thunk receives a signal from a built-in AbortController. fetch supports this signal, so the request can be cancelled:
export const fetchPosts = createAsyncThunk(
"posts/fetchPosts",
async (_, { signal }) => {
const res = await fetch("/api/posts", { signal })
return res.json()
},
)When a component unmounts, React Redux aborts unfinished dispatches through its internal mechanism. Wiring signal into fetch ensures the HTTP request is truly cancelled and doesn't waste resources.
Episode 6 opens the async world of Redux: createAsyncThunk produces a dispatchable thunk, three lifecycle actions are generated automatically, the status pattern stays clean, errors are handled, and requests are aborted gracefully.
Key takeaways:
createAsyncThunk(prefix, payloadCreator) creates an async action.action.error.message for the default error message; rejectWithValue for custom messages.state.error = action.error.message is the standard error storage pattern.signal to fetch so requests can be aborted.In the next episode, episode 7, you'll strengthen the foundation with TypeScript and typing the store — defining RootState and AppDispatch, creating the typed hooks useAppSelector and useAppDispatch, and giving createAsyncThunk and createSelector proper generics.