Error Handling Patterns
Handle runtime, network, and rendering failures consistently.
Recommended approach
- Use route-level and component-level error boundaries.
- Normalize API error shapes before UI rendering.
- Separate user-facing messages from internal diagnostics.
Alternatives and when to choose them
- Global-only error boundary for very small apps.
- Module-specific boundary hierarchy for complex products.
Implementation checklist
- Define common UI states: loading, empty, error, retry.
- Map backend error codes to user-safe messages.
- Capture frontend errors in monitoring with route context.
Practical examples
Route-level error boundary (React Router)
import { isRouteErrorResponse, useRouteError } from "react-router";
export default function ProjectPage() {
return <h1>Project details</h1>;
}
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<section role="alert">
<h1>
{error.status} {error.statusText}
</h1>
<p>Could not load this project route.</p>
</section>
);
}
const message = error instanceof Error ? error.message : "Unexpected error";
return (
<section role="alert">
<h1>Could not load this project</h1>
<p>{message}</p>
</section>
);
}Component-level error boundary (widget isolation)
import { ErrorBoundary } from "react-error-boundary";
export function ProjectSidebar() {
return (
<ErrorBoundary
fallbackRender={({ resetErrorBoundary }) => (
<div role="alert">
<p>Sidebar failed to render.</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<ProjectStatsWidget />
</ErrorBoundary>
);
}Data fetching errors with useQuery
import { useQuery } from "@tanstack/react-query";
function ProjectDetails({ projectId }: { projectId: string }) {
const query = useQuery({
queryKey: ["project", projectId],
queryFn: () => api.projects.getById(projectId),
retry: 1,
});
if (query.isLoading) return <p>Loading...</p>;
if (query.isError) {
return (
<div role="alert">
<p>Failed to load project.</p>
<button onClick={() => query.refetch()}>Retry</button>
</div>
);
}
return <ProjectView project={query.data} />;
}Mutation errors with useMutation
import { useMutation, useQueryClient } from "@tanstack/react-query";
type ApiError = {
status?: number;
message?: string;
};
function RenameProjectForm({ projectId }: { projectId: string }) {
const queryClient = useQueryClient();
const getUserMessage = (error: ApiError) => {
switch (error.status) {
case 400:
return "Invalid project name.";
case 401:
case 403:
return "You do not have permission to rename this project.";
case 409:
return "A project with this name already exists.";
case 422:
return "Validation failed. Check the input and try again.";
default:
return "Could not save changes. Please try again.";
}
};
const mutation = useMutation({
mutationFn: (name: string) => api.projects.rename(projectId, { name }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["project", projectId] });
},
});
const errorMessage = mutation.isError
? getUserMessage(mutation.error as ApiError)
: null;
return (
<form
onSubmit={(e) => {
e.preventDefault();
const name = new FormData(e.currentTarget).get("name") as string;
mutation.mutate(name);
}}
>
<input name="name" />
<button disabled={mutation.isPending} type="submit">
Save
</button>
{errorMessage ? <p role="alert">{errorMessage}</p> : null}
</form>
);
}Common pitfalls
- Showing raw backend errors to end users.
- Unhandled promise errors in async UI actions.