Lesson 21 of 30
An error boundary only catches what happens during render. A failed fetch, on its own, never gets there — someone has to explicitly throw it back into the tree.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A component fetches data in a useEffect and stores a caught fetch error in state, but does nothing else with it. Will a React error boundary wrapping this component catch that error?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A React error boundary catches errors thrown while rendering — including errors thrown in lifecycle methods and constructors of the class components below it in the tree. That's a narrower guarantee than it sounds like. An error inside an async function, or inside a .catch() handler attached to a fetch call, is not a rendering error. It happens on its own schedule, outside the render pass the boundary is watching, so the boundary never sees it. Left alone, that failure either turns into an unhandled promise rejection or gets swallowed by whatever catch block happened to be nearby — the user sees nothing, or sees stale data forever.
For an error boundary to actually catch a data-fetching failure, the failure has to be re-surfaced during render. Two ways that happens in practice:
Either way, the mechanism is identical to what makes render-time suspense-style loading work: an async result gets converted into something synchronous and throwable, timed to happen during render.
function useUserProfile(userId: string) {
const [state, setState] = useState<
{ status: "loading" } | { status: "error"; error: Error } | { status: "success"; data: Profile }
>({ status: "loading" });
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then((res) => {
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return res.json();
})
.then((data) => !cancelled && setState({ status: "success", data }))
.catch((error) => !cancelled && setState({ status: "error", error }));
return () => {
cancelled = true;
};
}, [userId]);
// The re-throw: this runs during render, not inside the async chain above.
if (state.status === "error") {
throw state.error;
}
return state;
}
function ProfileCard({ userId }: { userId: string }) {
const state = useUserProfile(userId);
if (state.status === "loading") return <Spinner />;
return <div>{state.data.name}</div>;
}
Wrapping ProfileCard in an error boundary now works, because the failure the hook caught in a .catch() gets converted into a synchronous throw the very next time the component renders. Without that last if block, the boundary sits there doing nothing while the error state quietly updates underneath it.
.catch() handler does not make an error boundary see it. The boundary only watches the render pass — an error handled entirely inside async code never touches render unless something explicitly re-throws it there."Where would you put your error boundaries, and how would a failed API call actually reach one?" tests whether a candidate understands error boundaries as a render-time mechanism specifically, not a generic try/catch for anything that can go wrong in a component.