Kurs Next.js · Moduł 6: Formularze i Server Actions
Komponenty error.tsx dla obsługi błędów
W tej lekcji9
Podczas tworzenia aplikacji w Next.js, jednym z ważnych aspektów jest odpowiednie obsługiwanie sytuacji awaryjnych. Framework Next.js oferuje prosty, a zarazem potężny mechanizm umożliwiający łatwe izolowanie i obsługę błędów w aplikacji - komponenty error.tsx. W tym module przyjrzymy się, jak efektywnie wykorzystać te komponenty do poprawy odporności i doświadczenia użytkownika naszej aplikacji.
Czym jest komponent error.tsx?
Komponent error.tsx to specjalny plik w strukturze aplikacji Next.js, który służy jako "granica błędu" (error boundary) dla danego segmentu aplikacji. Gdy w danym segmencie lub jego dzieciach wystąpi błąd:
- Next.js automatycznie przechwytuje ten błąd
- Renderuje komponent error.tsx zamiast komponentu, który wygenerował błąd
- Izoluje błąd do konkretnego segmentu, pozwalając reszcie aplikacji działać normalnie
Jak działa error.tsx w Next.js App Router?
W architekturze App Router, komponenty error.tsx możemy umieszczać na dowolnym poziomie struktury katalogów, co pozwala na bardzo precyzyjne określanie granic błędów.
Podstawowa implementacja
Oto jak wygląda prosty komponent error.tsx:
1'use client'; // Musi być komponentem klienckim
2
3import { useEffect } from 'react';
4
5interface ErrorComponentProps {
6 error: Error & { digest?: string };
7 reset: () => void;
8}
9
10export default function Error({ error, reset }: ErrorComponentProps) {
11 useEffect(() => {
12 // Opcjonalnie raportowanie błędu do serwisu monitorowania
13 console.error('Wystąpił nieoczekiwany błąd:', error);
14 }, [error]);
15
16 return (
17 <div className="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-md">
18 <h2 className="text-xl font-bold text-red-600 mb-4">Coś poszło nie tak!</h2>
19 <p className="text-gray-700 mb-4">
20 Przepraszamy, wystąpił nieoczekiwany błąd. Nasz zespół został powiadomiony.
21 </p>
22 <button
23 onClick={reset}
24 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
25 >
26 Spróbuj ponownie
27 </button>
28 </div>
29 );
30}Kluczowe elementy:
Oznaczenie 'use client' - komponent error.tsx musi być oznaczony jako komponent kliencki, nawet jeśli reszta aplikacji używa renderowania po stronie serwera.
Props:
error: Obiekt błędu zawierający informacje o tym, co poszło nie takreset: Funkcja, która pozwala na ponowną próbę renderowania komponentu, który wygenerował błąd
Funkcja reset(): Pozwala użytkownikom spróbować ponownie wykonać operację bez przeładowywania całej strony.
Strategie obsługi błędów z użyciem error.tsx
1. Hierarchiczna obsługa błędów
Next.js pozwala na tworzenie komponentów error.tsx na różnych poziomach struktury katalogów, co umożliwia precyzyjną kontrolę nad tym, jak obsługiwane są błędy w różnych częściach aplikacji.
1app/
2├── error.tsx # Globalny fallback dla błędów całej aplikacji
3├── layout.tsx
4├── page.tsx
5├── dashboard/
6│ ├── error.tsx # Obsługa błędów dla całego dashboardu
7│ ├── layout.tsx
8│ ├── page.tsx
9│ └── settings/
10│ ├── error.tsx # Specyficzny dla sekcji ustawień
11│ └── page.tsx
12└── profile/
13 ├── error.tsx # Specyficzny dla profilu użytkownika
14 └── page.tsx2. Szczegółowe komunikaty błędów w środowisku deweloperskim
Możemy dostosować komponent error.tsx tak, aby wyświetlał bardziej szczegółowe informacje o błędzie w środowisku deweloperskim:
1'use client';
2
3export default function Error({ error, reset }) {
4 const isDevelopment = process.env.NODE_ENV === 'development';
5
6 return (
7 <div>
8 <h2>Coś poszło nie tak!</h2>
9
10 {isDevelopment && (
11 <div className="bg-gray-100 p-4 rounded my-4">
12 <h3 className="font-bold">Szczegóły błędu (tylko w trybie dev):</h3>
13 <p className="font-mono">{error.message}</p>
14 <pre className="overflow-auto text-sm mt-2">{error.stack}</pre>
15 </div>
16 )}
17
18 <button onClick={reset}>Spróbuj ponownie</button>
19 </div>
20 );
21}3. Różne typy błędów, różne komunikaty
Możemy dostosować wygląd i komunikaty w zależności od typu błędu:
1'use client';
2
3import { useEffect } from 'react';
4
5export default function Error({ error, reset }) {
6 // Logowanie błędu do systemu monitoringu
7 useEffect(() => {
8 console.error(error);
9 // logErrorToService(error);
10 }, [error]);
11
12 // Określenie typu błędu na podstawie wiadomości lub innych właściwości
13 const is404 = error.message?.includes('not found') || error.statusCode === 404;
14 const isAuthentication = error.message?.includes('unauthorized') || error.statusCode === 401;
15
16 return (
17 <div className="p-6 mx-auto max-w-md">
18 {is404 ? (
19 <>
20 <h2 className="text-xl font-bold">Zasób nie znaleziony</h2>
21 <p>Przepraszamy, ale żądany zasób nie istnieje.</p>
22 </>
23 ) : isAuthentication ? (
24 <>
25 <h2 className="text-xl font-bold">Wymagane uwierzytelnienie</h2>
26 <p>Musisz się zalogować, aby uzyskać dostęp do tego zasobu.</p>
27 <button className="mt-4 px-4 py-2 bg-blue-500 text-white rounded">
28 Zaloguj się
29 </button>
30 </>
31 ) : (
32 <>
33 <h2 className="text-xl font-bold text-red-600">Nieoczekiwany błąd</h2>
34 <p>Przepraszamy, wystąpił nieoczekiwany problem.</p>
35 </>
36 )}
37
38 <button
39 onClick={reset}
40 className="mt-4 px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
41 >
42 Spróbuj ponownie
43 </button>
44 </div>
45 );
46}Integracjaz systemami monitoringu błędów
W produkcyjnych aplikacjach niezbędne jest gromadzenie informacji o błędach. Komponenty error.tsx mogą być zintegrowane z zewnętrznymi systemami monitoringu błędów, takimi jak Sentry, LogRocket czy New Relic:
1'use client';
2
3import { useEffect } from 'react';
4import * as Sentry from '@sentry/nextjs';
5
6export default function Error({ error, reset }) {
7 useEffect(() => {
8 // Wysłanie informacji o błędzie do Sentry
9 Sentry.captureException(error);
10 }, [error]);
11
12 return (
13 <div>
14 <h2>Coś poszło nie tak!</h2>
15 <p>Nasz zespół został powiadomiony o problemie.</p>
16 <button onClick={reset}>Spróbuj ponownie</button>
17 </div>
18 );
19}Obsługa błędów API i fetch
Błędy podczas pobierania danych w komponentach Next.js można obsługiwać za pomocą error.tsx, ale warto pamiętać o kilku niuansach:
W komponentach serwerowych
1// app/products/page.tsx
2async function getProducts() {
3 const res = await fetch('https://api.example.com/products');
4
5 if (!res.ok) {
6 // Ten błąd zostanie przechwycony przez najbliższy error.tsx
7 throw new Error('Nie udało się pobrać produktów');
8 }
9
10 return res.json();
11}
12
13export default async function ProductsPage() {
14 const products = await getProducts();
15
16 return (
17 <div>
18 <h1>Nasze produkty</h1>
19 <ul>
20 {products.map(product => (
21 <li key={product.id}>{product.name}</li>
22 ))}
23 </ul>
24 </div>
25 );
26}W komponentach klienckich
1'use client';
2
3import { useState, useEffect } from 'react';
4import { useRouter } from 'next/navigation';
5
6export default function ClientProductsPage() {
7 const [products, setProducts] = useState([]);
8 const [error, setError] = useState(null);
9 const router = useRouter();
10
11 useEffect(() => {
12 async function fetchProducts() {
13 try {
14 const res = await fetch('/api/products');
15
16 if (!res.ok) {
17 throw new Error('Błąd pobierania danych');
18 }
19
20 const data = await res.json();
21 setProducts(data);
22 } catch (err) {
23 setError(err.message);
24 // Opcjonalnie - możemy ręcznie wywołać najbliższy error.tsx
25 // throw err; // To spowoduje, że Next.js użyje najbliższego error.tsx
26 }
27 }
28
29 fetchProducts();
30 }, []);
31
32 if (error) {
33 return (
34 <div className="p-4 bg-red-50 border border-red-200 rounded">
35 <h2 className="text-red-600 font-bold">Wystąpił błąd</h2>
36 <p>{error}</p>
37 <button onClick={() => router.refresh()}>Odśwież stronę</button>
38 </div>
39 );
40 }
41
42 return (
43 <div>
44 <h1>Produkty (Klient)</h1>
45 {/* ... */}
46 </div>
47 );
48}Dobre praktyki przy pracy z error.tsx
Odpowiednia granularność - używaj error.tsx na poziomach, które dają sensowną izolację błędów, ale nie twórz zbyt wielu komponentów.
Użyteczne komunikaty - dostarczaj przyjazne dla użytkownika wiadomości i jasne instrukcje co do dalszych kroków.
Opcje naprawy - oferuj możliwości naprawy sytuacji, takie jak przycisk "Spróbuj ponownie" (używając funkcji
reset).Śledzenie i monitorowanie - zawsze loguj błędy do systemów monitorowania, aby móc śledzić i rozwiązywać problemy.
Testowanie - testuj różne scenariusze błędów, aby upewnić się, że Twoje granice błędów reagują prawidłowo.
Dostępność - zapewnij, że komunikaty błędów są dostępne również dla użytkowników korzystających z technologii wspomagających.
Łapanie błędów z layout.tsx
Granice błędów z error.tsx nie przechwytują błędów z komponentów layout.tsx w tym samym segmencie. Aby obsłużyć błędy w layoutach, musisz utworzyć komponent error.tsx w segmencie nadrzędnym.
1app/
2├── dashboard/
3│ ├── layout.tsx // Błędy tu nie są łapane przez error.tsx na tym samym poziomie
4│ ├── error.tsx // Ten nie złapie błędów z layout.tsx
5│ └── page.tsxPrawidłowa struktura do łapania błędów z layout.tsx:
1app/
2├── error.tsx // Ten złapie błędy z dashboard/layout.tsx
3├── dashboard/
4│ ├── layout.tsx
5│ └── page.tsxPrzykład kompletnej implementacji
Poniżej znajduje się przykład bardziej zaawansowanego komponentu error.tsx z obsługą różnych typów błędów, opcjami naprawy i integracją z zewnętrznym systemem monitorowania:
1'use client';
2
3import { useEffect, useState } from 'react';
4import Link from 'next/link';
5import { useRouter } from 'next/navigation';
6
7// Typy błędów, które możemy obsługiwać specjalnie
8enum ErrorType {
9 NOT_FOUND = 'NOT_FOUND',
10 UNAUTHORIZED = 'UNAUTHORIZED',
11 FORBIDDEN = 'FORBIDDEN',
12 TIMEOUT = 'TIMEOUT',
13 SERVER_ERROR = 'SERVER_ERROR',
14 UNKNOWN = 'UNKNOWN'
15}
16
17interface ErrorComponentProps {
18 error: Error & {
19 digest?: string;
20 statusCode?: number;
21 };
22 reset: () => void;
23}
24
25export default function Error({ error, reset }: ErrorComponentProps) {
26 const router = useRouter();
27 const [errorType, setErrorType] = useState<ErrorType>(ErrorType.UNKNOWN);
28 const [isReporting, setIsReporting] = useState(false);
29
30 // Określenie typu błędu
31 useEffect(() => {
32 if (error.message?.toLowerCase().includes('not found') || error.statusCode === 404) {
33 setErrorType(ErrorType.NOT_FOUND);
34 } else if (error.message?.toLowerCase().includes('unauthorized') || error.statusCode === 401) {
35 setErrorType(ErrorType.UNAUTHORIZED);
36 } else if (error.message?.toLowerCase().includes('forbidden') || error.statusCode === 403) {
37 setErrorType(ErrorType.FORBIDDEN);
38 } else if (error.message?.toLowerCase().includes('timeout') || error.message?.includes('timed out')) {
39 setErrorType(ErrorType.TIMEOUT);
40 } else if (error.statusCode >= 500 && error.statusCode < 600) {
41 setErrorType(ErrorType.SERVER_ERROR);
42 }
43 }, [error]);
44
45 // Raportowanie błędu
46 useEffect(() => {
47 // Logowanie do konsoli w dev
48 console.error('Application error:', error);
49
50 // W prawdziwej aplikacji wysłalibyśmy to do zewnętrznego serwisu
51 async function reportError() {
52 try {
53 setIsReporting(true);
54 // Przykładowa implementacja
55 // await fetch('/api/error-reporting', {
56 // method: 'POST',
57 // headers: { 'Content-Type': 'application/json' },
58 // body: JSON.stringify({
59 // message: error.message,
60 // stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
61 // url: window.location.href,
62 // type: errorType,
63 // timestamp: new Date().toISOString()
64 // })
65 // });
66 await new Promise(resolve => setTimeout(resolve, 1000)); // Symulacja
67 } catch (e) {
68 console.error('Failed to report error:', e);
69 } finally {
70 setIsReporting(false);
71 }
72 }
73
74 reportError();
75 }, [error, errorType]);
76
77 // Renderowanie różnych komunikatów w zależności od typu błędu
78 function renderErrorContent() {
79 switch (errorType) {
80 case ErrorType.NOT_FOUND:
81 return (
82 <>
83 <h2 className="text-2xl font-bold text-gray-800 mb-3">Nie znaleziono zasobu</h2>
84 <p className="text-gray-600 mb-4">
85 Strona, której szukasz, nie istnieje lub została przeniesiona.
86 </p>
87 <div className="flex space-x-4">
88 <button
89 onClick={() => router.back()}
90 className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
91 >
92 Powrót
93 </button>
94 <Link href="/" className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
95 Strona główna
96 </Link>
97 </div>
98 </>
99 );
100
101 case ErrorType.UNAUTHORIZED:
102 return (
103 <>
104 <h2 className="text-2xl font-bold text-gray-800 mb-3">Wymagane logowanie</h2>
105 <p className="text-gray-600 mb-4">
106 Musisz się zalogować, aby uzyskać dostęp do tej strony.
107 </p>
108 <Link
109 href="/login"
110 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
111 >
112 Zaloguj się
113 </Link>
114 </>
115 );
116
117 case ErrorType.FORBIDDEN:
118 return (
119 <>
120 <h2 className="text-2xl font-bold text-gray-800 mb-3">Brak dostępu</h2>
121 <p className="text-gray-600 mb-4">
122 Nie masz uprawnień do tej strony lub zasobu.
123 </p>
124 <button
125 onClick={() => router.back()}
126 className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
127 >
128 Powrót
129 </button>
130 </>
131 );
132
133 case ErrorType.TIMEOUT:
134 return (
135 <>
136 <h2 className="text-2xl font-bold text-gray-800 mb-3">Upłynął limit czasu</h2>
137 <p className="text-gray-600 mb-4">
138 Operacja trwała zbyt długo. Sprawdź swoje połączenie internetowe i spróbuj ponownie.
139 </p>
140 <button
141 onClick={reset}
142 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
143 >
144 Spróbuj ponownie
145 </button>
146 </>
147 );
148
149 case ErrorType.SERVER_ERROR:
150 return (
151 <>
152 <h2 className="text-2xl font-bold text-gray-800 mb-3">Błąd serwera</h2>
153 <p className="text-gray-600 mb-4">
154 Przepraszamy, wystąpił błąd po stronie serwera. Nasz zespół został powiadomiony.
155 </p>
156 <button
157 onClick={reset}
158 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
159 >
160 Spróbuj ponownie
161 </button>
162 </>
163 );
164
165 default:
166 return (
167 <>
168 <h2 className="text-2xl font-bold text-gray-800 mb-3">Coś poszło nie tak</h2>
169 <p className="text-gray-600 mb-4">
170 Przepraszamy, wystąpił nieoczekiwany błąd. {isReporting ? 'Zgłaszamy problem...' : 'Nasz zespół został powiadomiony.'}
171 </p>
172 <div className="flex space-x-4">
173 <button
174 onClick={reset}
175 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
176 >
177 Spróbuj ponownie
178 </button>
179 <button
180 onClick={() => router.refresh()}
181 className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
182 >
183 Odśwież stronę
184 </button>
185 </div>
186 </>
187 );
188 }
189 }
190
191 // Rendering dla trybu deweloperskiego - dodatkowe informacje o błędzie
192 function renderDevModeInfo() {
193 if (process.env.NODE_ENV !== 'development') return null;
194
195 return (
196 <div className="mt-8 p-4 bg-gray-100 rounded-lg border border-gray-300">
197 <h3 className="text-sm font-bold uppercase text-gray-500 mb-2">
198 Informacje o błędzie (tylko tryb dev)
199 </h3>
200 <p className="font-mono text-sm mb-2">{error.message}</p>
201 {error.digest && (
202 <p className="font-mono text-xs text-gray-500 mb-2">Digest: {error.digest}</p>
203 )}
204 {error.stack && (
205 <pre className="mt-2 p-2 bg-gray-800 text-gray-200 rounded overflow-x-auto text-xs">
206 {error.stack}
207 </pre>
208 )}
209 </div>
210 );
211 }
212
213 return (
214 <div className="min-h-[400px] flex flex-col items-center justify-center p-6">
215 <div className="w-full max-w-md bg-white rounded-lg shadow-lg p-8 text-center">
216 {renderErrorContent()}
217 {renderDevModeInfo()}
218 </div>
219 </div>
220 );
221}Podsumowanie
Komponenty error.tsx w Next.js to potężne narzędzie do obsługi błędów, które:
- Izolują błędy do konkretnych segmentów aplikacji
- Pozwalają na prezentowanie przyjaznych komunikatów użytkownikom
- Umożliwiają próbę naprawy sytuacji bez przeładowywania strony
- Mogą być dostosowane dla różnych typów błędów
- Pozwalają na integrację z systemami monitoringu błędów
Dzięki nim możesz tworzyć bardziej odporne aplikacje, które elegancko obsługują sytuacje awaryjne, poprawiając ogólne doświadczenie użytkownika i zmniejszając frustrację związaną z nieoczekiwanymi problemami.
Kod do tej lekcji: App.tsx
1import React, { useState, Component, ReactNode } from 'react';
2
3// Symulacja error.tsx w Next.js App Router
4// Kazdy segment moze miec wlasny error boundary
5
6// Symulowany error boundary (jak error.tsx w Next.js)
7class ErrorBoundary extends Component<
8 { children: ReactNode; fallback: (error: Error, reset: () => void) => ReactNode },
9 { error: Error | null }
10> {
11 state: { error: Error | null } = { error: null };
12 static getDerivedStateFromError(error: Error) { return { error }; }
13 reset = () => this.setState({ error: null });
14 render() {
15 if (this.state.error) return this.props.fallback(this.state.error, this.reset);
16 return this.props.children;
17 }
18}
19
20// Komponent error.tsx - wyswietlany przy bledzie
21function ErrorComponent({ error, reset }: { error: Error; reset: () => void }) {
22 return (
23 <div style={{
24 background: '#3e1010', border: '1px solid #f44336', borderRadius: 8,
25 padding: 16, margin: '8px 0'
26 }}>
27 <h3 style={{ color: '#f44336', margin: '0 0 8px' }}>error.tsx</h3>
28 <p style={{ color: '#ffcdd2', fontSize: 13 }}>{error.message}</p>
29 <button onClick={reset} style={{
30 background: '#f44336', color: '#fff', border: 'none', borderRadius: 4,
31 padding: '6px 12px', cursor: 'pointer', fontFamily: 'monospace', marginTop: 8
32 }}>
33 Sprobuj ponownie (reset)
34 </button>
35 </div>
36 );
37}
38
39// Komponent, ktory moze rzucic blad
40function DangerousWidget({ label }: { label: string }) {
41 const [shouldError, setShouldError] = useState(false);
42 if (shouldError) throw new Error(`Blad w komponencie "${label}"!`);
43 return (
44 <div style={{ background: '#1a2744', borderRadius: 8, padding: 16, border: '1px solid #64ffda' }}>
45 <h4 style={{ color: '#64ffda', margin: '0 0 8px' }}>{label}</h4>
46 <p style={{ color: '#aaa', fontSize: 12 }}>Ten komponent dziala poprawnie.</p>
47 <button onClick={() => setShouldError(true)} style={{
48 background: '#ff5722', color: '#fff', border: 'none', borderRadius: 4,
49 padding: '4px 10px', cursor: 'pointer', fontFamily: 'monospace', fontSize: 11
50 }}>
51 Wywolaj blad
52 </button>
53 </div>
54 );
55}
56
57export default function ErrorTsxDemo() {
58 return (
59 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: 20, color: '#e0e0e0', fontFamily: 'monospace' }}>
60 <h1 style={{ color: '#64ffda' }}>error.tsx w Next.js</h1>
61 <p style={{ color: '#888', fontSize: 13, marginBottom: 16 }}>
62 Kazdy segment moze miec wlasny error.tsx. Blad izolowany - reszta aplikacji dziala.
63 </p>
64
65 <div style={{ display: 'grid', gap: 12 }}>
66 <div style={{ background: '#0d1117', borderRadius: 8, padding: 12 }}>
67 <div style={{ color: '#888', fontSize: 11, marginBottom: 8 }}>app/dashboard/error.tsx</div>
68 <ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} />}>
69 <DangerousWidget label="Dashboard Widget" />
70 </ErrorBoundary>
71 </div>
72
73 <div style={{ background: '#0d1117', borderRadius: 8, padding: 12 }}>
74 <div style={{ color: '#888', fontSize: 11, marginBottom: 8 }}>app/profile/error.tsx</div>
75 <ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} />}>
76 <DangerousWidget label="Profile Widget" />
77 </ErrorBoundary>
78 </div>
79
80 <div style={{ background: '#0d1117', borderRadius: 8, padding: 12 }}>
81 <div style={{ color: '#888', fontSize: 11, marginBottom: 8 }}>app/settings/error.tsx (niezalezny)</div>
82 <ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} />}>
83 <DangerousWidget label="Settings Widget" />
84 </ErrorBoundary>
85 </div>
86 </div>
87 </div>
88 );
89}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. ARIA labels w formularzach pomagają w: