Skip to content

Commit 52e044d

Browse files
nullcoderClaude
andauthored
feat: implement ErrorBoundary component for graceful error handling (#86)
Add comprehensive ErrorBoundary implementation with app-level integration, custom fallback UI support, and proper error logging. Includes error containment at multiple levels and demo page for testing error scenarios. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <claude@ghostpaste.dev>
1 parent 2b018bf commit 52e044d

File tree

6 files changed

+992
-23
lines changed

6 files changed

+992
-23
lines changed

app/demo/error-boundary/page.tsx

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Card,
7+
CardContent,
8+
CardDescription,
9+
CardHeader,
10+
CardTitle,
11+
} from "@/components/ui/card";
12+
import { ErrorBoundary, withErrorBoundary } from "@/components/error-boundary";
13+
import { AppError, ErrorCode } from "@/types/errors";
14+
15+
// Component that throws an error when triggered
16+
function ErrorThrower({
17+
shouldThrow = false,
18+
errorType = "generic",
19+
}: {
20+
shouldThrow?: boolean;
21+
errorType?: string;
22+
}) {
23+
if (shouldThrow) {
24+
switch (errorType) {
25+
case "app-error":
26+
throw new AppError(
27+
ErrorCode.DECRYPTION_FAILED,
28+
400,
29+
"This is a custom app error for testing"
30+
);
31+
case "chunk-error":
32+
const chunkError = new Error("Loading chunk 123 failed");
33+
chunkError.name = "ChunkLoadError";
34+
throw chunkError;
35+
case "network-error":
36+
throw new Error("Network request failed");
37+
case "generic":
38+
default:
39+
throw new Error("This is a generic test error");
40+
}
41+
}
42+
return (
43+
<div className="p-4 text-green-600">✅ Component is working fine!</div>
44+
);
45+
}
46+
47+
// Component wrapped with HOC
48+
const WrappedErrorThrower = withErrorBoundary(ErrorThrower, {
49+
showReset: true,
50+
showHome: false,
51+
});
52+
53+
export default function ErrorBoundaryDemo() {
54+
const [globalError, setGlobalError] = useState(false);
55+
const [globalErrorType, setGlobalErrorType] = useState("generic");
56+
const [localError, setLocalError] = useState(false);
57+
const [localErrorType, setLocalErrorType] = useState("generic");
58+
const [hocError, setHocError] = useState(false);
59+
const [hocErrorType, setHocErrorType] = useState("generic");
60+
61+
const handleCustomFallback = (error: Error) => (
62+
<div className="rounded-md border border-purple-200 bg-purple-50 p-4 dark:border-purple-800 dark:bg-purple-950/20">
63+
<h3 className="font-medium text-purple-800 dark:text-purple-200">
64+
Custom Error Handler
65+
</h3>
66+
<p className="mt-1 text-sm text-purple-700 dark:text-purple-300">
67+
Caught: {error.message}
68+
</p>
69+
</div>
70+
);
71+
72+
const errorTypes = [
73+
{ value: "generic", label: "Generic Error" },
74+
{ value: "app-error", label: "App Error (with code)" },
75+
{ value: "chunk-error", label: "Chunk Load Error" },
76+
{ value: "network-error", label: "Network Error" },
77+
];
78+
79+
return (
80+
<div className="container mx-auto max-w-6xl p-6">
81+
<div className="space-y-8">
82+
<div>
83+
<h1 className="text-3xl font-bold tracking-tight">
84+
ErrorBoundary Demo
85+
</h1>
86+
<p className="text-muted-foreground mt-2">
87+
Demo of the ErrorBoundary component with different error scenarios
88+
and configurations.
89+
</p>
90+
</div>
91+
92+
<div className="grid gap-6 lg:grid-cols-2">
93+
{/* Global Error Boundary Test */}
94+
<Card>
95+
<CardHeader>
96+
<CardTitle>Global Error Boundary</CardTitle>
97+
<CardDescription>
98+
Test the app-level error boundary that catches all unhandled
99+
errors.
100+
</CardDescription>
101+
</CardHeader>
102+
<CardContent className="space-y-4">
103+
<div className="space-y-2">
104+
<label className="text-sm font-medium">Error Type:</label>
105+
<select
106+
value={globalErrorType}
107+
onChange={(e) => setGlobalErrorType(e.target.value)}
108+
className="border-input bg-background w-full rounded-md border px-3 py-2 text-sm"
109+
>
110+
{errorTypes.map((type) => (
111+
<option key={type.value} value={type.value}>
112+
{type.label}
113+
</option>
114+
))}
115+
</select>
116+
</div>
117+
118+
<Button
119+
onClick={() => setGlobalError(true)}
120+
variant="destructive"
121+
className="w-full"
122+
>
123+
Trigger Global Error
124+
</Button>
125+
126+
<div className="rounded-md border p-4">
127+
<ErrorThrower
128+
shouldThrow={globalError}
129+
errorType={globalErrorType}
130+
/>
131+
</div>
132+
</CardContent>
133+
</Card>
134+
135+
{/* Local Error Boundary Test */}
136+
<Card>
137+
<CardHeader>
138+
<CardTitle>Local Error Boundary</CardTitle>
139+
<CardDescription>
140+
Test a local error boundary that only catches errors in a
141+
specific component tree.
142+
</CardDescription>
143+
</CardHeader>
144+
<CardContent className="space-y-4">
145+
<div className="space-y-2">
146+
<label className="text-sm font-medium">Error Type:</label>
147+
<select
148+
value={localErrorType}
149+
onChange={(e) => setLocalErrorType(e.target.value)}
150+
className="border-input bg-background w-full rounded-md border px-3 py-2 text-sm"
151+
>
152+
{errorTypes.map((type) => (
153+
<option key={type.value} value={type.value}>
154+
{type.label}
155+
</option>
156+
))}
157+
</select>
158+
</div>
159+
160+
<Button
161+
onClick={() => setLocalError(true)}
162+
variant="destructive"
163+
className="w-full"
164+
>
165+
Trigger Local Error
166+
</Button>
167+
168+
<ErrorBoundary>
169+
<div className="rounded-md border p-4">
170+
<ErrorThrower
171+
shouldThrow={localError}
172+
errorType={localErrorType}
173+
/>
174+
</div>
175+
</ErrorBoundary>
176+
</CardContent>
177+
</Card>
178+
179+
{/* Custom Fallback Test */}
180+
<Card>
181+
<CardHeader>
182+
<CardTitle>Custom Fallback UI</CardTitle>
183+
<CardDescription>
184+
Test error boundary with a custom fallback component instead of
185+
the default UI.
186+
</CardDescription>
187+
</CardHeader>
188+
<CardContent className="space-y-4">
189+
<Button
190+
onClick={() => setHocError(true)}
191+
variant="destructive"
192+
className="w-full"
193+
>
194+
Trigger Custom Fallback
195+
</Button>
196+
197+
<ErrorBoundary fallback={handleCustomFallback}>
198+
<div className="rounded-md border p-4">
199+
<ErrorThrower shouldThrow={hocError} errorType="generic" />
200+
</div>
201+
</ErrorBoundary>
202+
</CardContent>
203+
</Card>
204+
205+
{/* HOC Error Boundary Test */}
206+
<Card>
207+
<CardHeader>
208+
<CardTitle>HOC Error Boundary</CardTitle>
209+
<CardDescription>
210+
Test the withErrorBoundary higher-order component wrapper.
211+
</CardDescription>
212+
</CardHeader>
213+
<CardContent className="space-y-4">
214+
<div className="space-y-2">
215+
<label className="text-sm font-medium">Error Type:</label>
216+
<select
217+
value={hocErrorType}
218+
onChange={(e) => setHocErrorType(e.target.value)}
219+
className="border-input bg-background w-full rounded-md border px-3 py-2 text-sm"
220+
>
221+
{errorTypes.map((type) => (
222+
<option key={type.value} value={type.value}>
223+
{type.label}
224+
</option>
225+
))}
226+
</select>
227+
</div>
228+
229+
<Button
230+
onClick={() => setHocError(true)}
231+
variant="destructive"
232+
className="w-full"
233+
>
234+
Trigger HOC Error
235+
</Button>
236+
237+
<div className="rounded-md border p-4">
238+
<WrappedErrorThrower
239+
shouldThrow={hocError}
240+
errorType={hocErrorType}
241+
/>
242+
</div>
243+
</CardContent>
244+
</Card>
245+
</div>
246+
247+
<div className="space-y-4">
248+
<h2 className="text-xl font-semibold">Features Demonstrated</h2>
249+
<div className="grid gap-4 md:grid-cols-2">
250+
<div className="space-y-2">
251+
<h3 className="font-medium">✅ Error Handling Features</h3>
252+
<ul className="text-muted-foreground space-y-1 text-sm">
253+
<li>• React Error Boundary implementation</li>
254+
<li>• Integration with app error system</li>
255+
<li>• Custom error message formatting</li>
256+
<li>• Error logging to console and logger</li>
257+
<li>• Recovery actions (reset, go home)</li>
258+
<li>• Development vs production error details</li>
259+
</ul>
260+
</div>
261+
<div className="space-y-2">
262+
<h3 className="font-medium">🎨 UI Features</h3>
263+
<ul className="text-muted-foreground space-y-1 text-sm">
264+
<li>• Consistent with shadcn/ui design system</li>
265+
<li>• Dark/light theme support</li>
266+
<li>• Mobile-responsive layout</li>
267+
<li>• Accessibility support (ARIA, keyboard nav)</li>
268+
<li>• Custom fallback component support</li>
269+
<li>• Higher-order component wrapper</li>
270+
</ul>
271+
</div>
272+
</div>
273+
</div>
274+
275+
<div className="space-y-4">
276+
<h2 className="text-xl font-semibold">Error Types</h2>
277+
<div className="grid gap-4 md:grid-cols-2">
278+
<div className="space-y-2">
279+
<h3 className="font-medium">🔴 Error Categories</h3>
280+
<ul className="text-muted-foreground space-y-1 text-sm">
281+
<li>
282+
<strong>Generic:</strong> Standard JavaScript errors
283+
</li>
284+
<li>
285+
<strong>App Error:</strong> Custom errors with error codes
286+
</li>
287+
<li>
288+
<strong>Chunk Load:</strong> Application resource loading
289+
failures
290+
</li>
291+
<li>
292+
<strong>Network:</strong> Network connectivity issues
293+
</li>
294+
</ul>
295+
</div>
296+
<div className="space-y-2">
297+
<h3 className="font-medium">🛠️ Recovery Actions</h3>
298+
<ul className="text-muted-foreground space-y-1 text-sm">
299+
<li>
300+
<strong>Try Again:</strong> Resets error boundary state
301+
</li>
302+
<li>
303+
<strong>Go Home:</strong> Navigates to homepage
304+
</li>
305+
<li>
306+
<strong>Custom Fallback:</strong> Alternative error UI
307+
</li>
308+
<li>
309+
<strong>Error Logging:</strong> Automatic error reporting
310+
</li>
311+
</ul>
312+
</div>
313+
</div>
314+
</div>
315+
</div>
316+
</div>
317+
);
318+
}

app/layout.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
22
import { Geist, Geist_Mono } from "next/font/google";
33
import { ThemeProvider } from "@/components/theme-provider";
44
import { Header } from "@/components/header";
5+
import { ErrorBoundary } from "@/components/error-boundary";
56
import "./globals.css";
67

78
const geistSans = Geist({
@@ -36,10 +37,12 @@ export default function RootLayout({
3637
enableSystem
3738
disableTransitionOnChange
3839
>
39-
<Header />
40-
<main id="main-content" className="flex-1">
41-
{children}
42-
</main>
40+
<ErrorBoundary>
41+
<Header />
42+
<main id="main-content" className="flex-1">
43+
<ErrorBoundary>{children}</ErrorBoundary>
44+
</main>
45+
</ErrorBoundary>
4346
</ThemeProvider>
4447
</body>
4548
</html>

0 commit comments

Comments
 (0)