In the previous chapter, we created a Suspense-enabled UserGrid component. However, we didn’t handle errors. If the data fetch fails, useAtomSuspense throws the error and the app crashes.
To catch these errors and prevent crashes, React provides Error Boundaries. They wrap a section of your component tree and display a fallback UI when any child component throws.
Using React Error Boundary
First, install the react-error-boundary package:
pnpm add react-error-boundaryCreate a new file components/error-fallback.tsx:
"use client";
import type { FallbackProps } from "react-error-boundary";
import { FailureCard } from "@/components/failure-card";
import { getErrorInfo } from "@/lib/utils";
export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { const { title, message } = getErrorInfo(error);
return ( <FailureCard title={title} message={message} onRetry={resetErrorBoundary} /> );}Now update app/page.tsx to wrap the Suspense boundary with an Error Boundary:
"use client";
import { Suspense } from "react";import { useAtomRefresh } from "@effect-atom/atom-react";import { ErrorBoundary } from "react-error-boundary";import { Link } from "react-transition-progress/next";
import { ErrorFallback } from "@/components/error-fallback";import { UserGridSpinner } from "@/components/user-grid-spinner";import { UserGridSuspense } from "@/components/user-grid-suspense";import { UserPagination } from "@/components/user-pagination";import { UserSearchBar } from "@/components/user-search-bar";
import { usersAtom } from "@/atoms/user";
export default function HomePage() { const refreshUsers = useAtomRefresh(usersAtom);
return ( <div className="container max-w-5xl space-y-10"> <div className="flex items-center justify-between"> <h1>Users</h1> <Link href="/add-user" className="bg-accent text-foreground px-4 py-2 text-sm font-medium transition-colors hover:bg-neutral-200" > + Add User </Link> </div> <UserSearchBar /> <ErrorBoundary FallbackComponent={ErrorFallback} onReset={refreshUsers}> <Suspense fallback={<UserGridSpinner />}> <UserGridSuspense /> </Suspense> </ErrorBoundary> <UserPagination /> </div> );}The Error Boundary wraps the Suspense boundary. If the data fetch fails, the Error Boundary catches the error and renders ErrorFallback instead of crashing the app.
Notice that we use the useAtomRefresh hook. This hook takes an Atom and returns a function that, when called, tells the Registry to invalidate the Atom and re-run its Effect. We store this function in refreshUsers.
We then pass refreshUsers as the onReset prop of the ErrorBoundary. When the user clicks the retry button in ErrorFallback, it calls resetErrorBoundary, which does two things. First, it resets the Error Boundary’s internal state so it attempts to render its children again. Second, it triggers the onReset callback, which calls refreshUsers to re-run the usersAtom’s Effect. Without onReset, the Error Boundary would try to re-render the children, but the Atom’s failed result would still be cached. It would throw the same error again immediately.
To see the Error Boundary in action, go to services/user-service.ts and change the endpoint from /users to /usersx (an endpoint that doesn’t exist). Visit the home page. You’ll see the ErrorFallback component rendered with the error title and message. Click the retry button. Since the endpoint is still invalid, you’ll see the error again.
Handling Errors Manually
If you prefer to handle errors within the component rather than using an Error Boundary, useAtomSuspense accepts an includeFailure option.
"use client";
import { Suspense } from "react";import { useAtomRefresh } from "@effect-atom/atom-react";import { ErrorBoundary } from "react-error-boundary";import { Link } from "react-transition-progress/next";
import { ErrorFallback } from "@/components/error-fallback";import { UserGridSpinner } from "@/components/user-grid-spinner";import { UserGridSuspense } from "@/components/user-grid-suspense";import { UserPagination } from "@/components/user-pagination";import { UserSearchBar } from "@/components/user-search-bar";
import { usersAtom } from "@/atoms/user";
export default function HomePage() { const refreshUsers = useAtomRefresh(usersAtom);
return ( <div className="container max-w-5xl space-y-10"> <div className="flex items-center justify-between"> <h1>Users</h1> <Link href="/add-user" className="bg-accent text-foreground px-4 py-2 text-sm font-medium transition-colors hover:bg-neutral-200" > + Add User </Link> </div> <UserSearchBar /> <ErrorBoundary FallbackComponent={ErrorFallback} onReset={refreshUsers}> <Suspense fallback={<UserGridSpinner />}> <UserGridSuspense /> </Suspense> </ErrorBoundary> <UserPagination /> </div> );}Now replace the code inside components/user-grid-suspense.tsx with the following:
"use client";
import { useAtomRefresh, useAtomSuspense } from "@effect-atom/atom-react";import { Cause } from "effect";
import { FailureCard } from "@/components/failure-card";import { UserSuccessCard } from "@/components/user-success-card";
import { getErrorInfo } from "@/lib/utils";import { usersAtom } from "@/atoms/user";
export function UserGridSuspense() { const result = useAtomSuspense(usersAtom, { includeFailure: true }); const refresh = useAtomRefresh(usersAtom);
if (result._tag === "Failure") { const error = Cause.squash(result.cause); const { title, message } = getErrorInfo(error);
return <FailureCard title={title} message={message} onRetry={refresh} />; }
return ( <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> {result.value.users.map((user) => ( <UserSuccessCard key={user.id} user={user} /> ))} </div> );}With includeFailure: true, the useAtomSuspense hook returns the Failure result instead of throwing. This gives you full control over how errors are displayed.
Cause.squash extracts the underlying error from the Cause. This is necessary because Effect wraps errors in a Cause type that can represent complex error scenarios like multiple failures or interruptions.
If you still have the endpoint set to /usersx, visit the home page. You’ll see the FailureCard component rendered with the error details and a retry button, handled entirely within the component rather than by an Error Boundary.
Before continuing, make sure to revert the users endpoint back to /users in
services/user-service.ts.