Interview question
Build a recoverable route error boundary
Builds a route-level Next.js failure UI with safe reporting, retry, and an escape route while keeping expected errors out of the boundary.
TL;DR
Builds a route-level Next.js failure UI with safe reporting, retry, and an escape route while keeping expected errors out of the boundary.
Next.js error boundaries, Client Component requirements, reset behavior, expected versus unexpected errors, safe diagnostics, accessibility, and recovery testing.
Practice the problem like a real interview: restate, reason, implement, and test.
Create an App Router error.tsx boundary for one route segment. It must announce a concise failure message, report only a safe route and optional digest, call reset() when the user retries, and retain a link out of the failed segment. Do not expose the raw exception, retry automatically, or use the boundary for expected validation and API outcomes.
When an unexpected render failure reaches the boundary, the user sees We could not load this page, a Try again button, and a Back to drills link. One safe report contains /tracks/dotnet-full-stack and digest abc123, but not Database password leaked. Selecting Try again invokes reset() exactly once; a second failure remains recoverable instead of entering an automatic retry loop.
error.tsx must be a Client Component.reset() function once.error.tsx at the narrowest route segment that can recover independently."use client" because the boundary receives error and reset and uses an effect.useEffect; make the reporter tolerant of duplicate delivery and swallow telemetry failure locally.reset, navigation remains, and reporting failure does not break the fallback.// app/tracks/[trackSlug]/error.tsx
"use client";
import Link from "next/link";
import { useEffect } from "react";
import { reportFrontendError } from "@/lib/telemetry";
export default function TrackError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
void reportFrontendError({
digest: error.digest ?? null,
route: window.location.pathname,
}).catch(() => undefined);
}, [error]);
return (
<section role="alert" aria-labelledby="track-error-title">
<h1 id="track-error-title">We could not load this page</h1>
<p>Try this page again, or return to the drill catalog.</p>
{error.digest ? <p>Reference: {error.digest}</p> : null}
<button type="button" onClick={reset}>Try again</button>
<Link href="/drills">Back to drills</Link>
</section>
);
}
it("reports safely and lets the user retry", async () => {
const user = userEvent.setup();
const reset = vi.fn();
reportFrontendError.mockResolvedValue(undefined);
render(
<TrackError
error={Object.assign(new Error("Database password leaked"), { digest: "abc123" })}
reset={reset}
/>,
);
expect(screen.getByRole("alert")).toBeVisible();
expect(screen.queryByText(/Database password leaked/i)).not.toBeInTheDocument();
expect(screen.getByText(/Reference: abc123/i)).toBeVisible();
expect(screen.getByRole("link", { name: "Back to drills" })).toHaveAttribute("href", "/drills");
await waitFor(() => expect(reportFrontendError).toHaveBeenCalledWith({
digest: "abc123",
route: expect.any(String),
}));
await user.click(screen.getByRole("button", { name: "Try again" }));
expect(reset).toHaveBeenCalledTimes(1);
});
A segment boundary limits the blast radius and preserves parent layouts, but it cannot recover failures in its own layout or in event handlers that never reach the render boundary. A broad global boundary catches more, yet provides less contextual recovery. Telemetry should deduplicate by a stable event identity because effects and retries may report more than once; losing one telemetry call is preferable to replacing the recovery UI with another error.
reset() rerenders the segment but the underlying dependency still fails.reset() once and never loops automatically."use client", so it cannot use the boundary contract correctly.error.message or a stack trace and leaks implementation details.Next in Frontend Labs: Render Performance