Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Komponenty error.tsx dla obsługi błędów

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:

  1. Next.js automatycznie przechwytuje ten błąd
  2. Renderuje komponent error.tsx zamiast komponentu, który wygenerował błąd
  3. 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:

  1. Oznaczenie 'use client' - komponent error.tsx musi być oznaczony jako komponent kliencki, nawet jeśli reszta aplikacji używa renderowania po stronie serwera.

  2. Props:

    • error
      : Obiekt błędu zawierający informacje o tym, co poszło nie tak
    • reset
      : Funkcja, która pozwala na ponowną próbę renderowania komponentu, który wygenerował błąd
  3. 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.tsx

2. 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

  1. Odpowiednia granularność - używaj error.tsx na poziomach, które dają sensowną izolację błędów, ale nie twórz zbyt wielu komponentów.

  2. Użyteczne komunikaty - dostarczaj przyjazne dla użytkownika wiadomości i jasne instrukcje co do dalszych kroków.

  3. Opcje naprawy - oferuj możliwości naprawy sytuacji, takie jak przycisk "Spróbuj ponownie" (używając funkcji

    reset
    ).

  4. Śledzenie i monitorowanie - zawsze loguj błędy do systemów monitorowania, aby móc śledzić i rozwiązywać problemy.

  5. Testowanie - testuj różne scenariusze błędów, aby upewnić się, że Twoje granice błędów reagują prawidłowo.

  6. 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.tsx

Prawidł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.tsx

Przykł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:

  1. Izolują błędy do konkretnych segmentów aplikacji
  2. Pozwalają na prezentowanie przyjaznych komunikatów użytkownikom
  3. Umożliwiają próbę naprawy sytuacji bez przeładowywania strony
  4. Mogą być dostosowane dla różnych typów błędów
  5. 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.

Ir a CodeWorlds