Skip to content

ApiErrorBoundary

<ApiErrorBoundary> is a small class-based error boundary that catches errors thrown during render, types them as ApiError | unknown, and lets you reset back to a clean state.

It’s the partner to useSuspenseQ / useSuspenseInfiniteQuery. Throwing a Promise (suspending) propagates to <Suspense>; throwing an Error propagates here.

import { Suspense } from "react";
import { ApiErrorBoundary, isApiError } from "@use-q/api-client-react";
import { useSuspenseQ } from "@/api/client";
function PostList({ facilityId }: { facilityId: string }) {
const { data } = useSuspenseQ("listPosts", { pathParams: { facilityId } });
return data.map((p) => <article key={p.id}>{p.title}</article>);
}
export function Page({ facilityId }: { facilityId: string }) {
return (
<ApiErrorBoundary
fallback={(error, reset) => (
<div role="alert">
{isApiError(error)
? `${error.status}${error.message}`
: "Something went wrong."}
<button onClick={reset}>Retry</button>
</div>
)}
>
<Suspense fallback={<p>Loading posts…</p>}>
<PostList facilityId={facilityId} />
</Suspense>
</ApiErrorBoundary>
);
}
interface ApiErrorBoundaryProps {
children: React.ReactNode;
fallback: (error: unknown, reset: () => void) => React.ReactNode;
onError?: (error: unknown) => void;
onReset?: () => void;
}

Called whenever a render-time error is caught. You receive the raw error and a reset function.

Use isApiError to narrow the error to your domain shape:

<ApiErrorBoundary
fallback={(err, reset) => {
if (isApiError<ApiProblem>(err)) {
if (err.status === 404) return <NotFound />;
if (err.status === 403) return <Forbidden />;
return <ProblemAlert problem={err.parsed} onRetry={reset} />;
}
return <UnknownError onRetry={reset} />;
}}
>
{/* … */}
</ApiErrorBoundary>

A side-effecting hook that fires once per caught error. Use it for telemetry:

<ApiErrorBoundary
onError={(err) => {
Sentry.captureException(err);
}}
fallback={(err, reset) => <ErrorScreen error={err} onRetry={reset} />}
>

Called when the user (or your code) invokes reset. Use it to clear adjacent state or invalidate related caches.

<ApiErrorBoundary
onReset={() => qc.invalidateAll()}
fallback={(err, reset) => <ErrorScreen onRetry={reset} />}
>

reset() does exactly two things:

  1. Clears the boundary’s internal error state, causing it to re-render its children.
  2. Calls your optional onReset callback.

It does not automatically refetch. The next render will resume normally — if the underlying query is still in an errored state, the suspense child will throw again immediately. The typical pattern is to pair reset with a cache invalidation:

<ApiErrorBoundary
fallback={(err, reset) => {
const qc = useQClient();
return (
<button
onClick={() => {
void qc.invalidateAll();
reset();
}}
>
Try again
</button>
);
}}
>

Or wire it up via onReset:

function PageBoundary({ children }: { children: React.ReactNode }) {
const qc = useQClient();
return (
<ApiErrorBoundary
onReset={() => qc.invalidateAll()}
fallback={(err, reset) => (
<button onClick={reset}>Try again</button>
)}
>
{children}
</ApiErrorBoundary>
);
}

You can nest boundaries to make some parts of the page fail in isolation:

<ApiErrorBoundary fallback={() => <PageError />}>
<Suspense fallback={<PageSkeleton />}>
<Header />
<ApiErrorBoundary fallback={() => <CommentsUnavailable />}>
<Suspense fallback={<CommentsSkeleton />}>
<Comments postId={postId} />
</Suspense>
</ApiErrorBoundary>
</Suspense>
</ApiErrorBoundary>

The inner boundary catches errors from <Comments /> only; everything else keeps rendering.

isApiError is a generic type guard — pass the parsed-error type for full inference:

import { isApiError } from "@use-q/api-client-react";
interface ApiProblem {
type: string;
title: string;
detail: string;
}
fallback={(err, reset) => {
if (isApiError<ApiProblem>(err)) {
return (
<article>
<h2>{err.parsed.title}</h2>
<p>{err.parsed.detail}</p>
<button onClick={reset}>Retry</button>
</article>
);
}
return <p>Unknown error</p>;
}}