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.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: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.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}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łądFunkcja reset(): Pozwala użytkownikom spróbować ponownie wykonać operację bez przeładowywania całej strony.
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.tsxMoż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}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}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}Błędy podczas pobierania danych w komponentach Next.js można obsługiwać za pomocą error.tsx, ale warto pamiętać o kilku niuansach:
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}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}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.
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.tsxPoniż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}Komponenty error.tsx w Next.js to potężne narzędzie do obsługi błędów, które:
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.