Kurs JavaScript i React · Moduł 16: Obsługa błędów i Suspense

Error Boundaries - straże twojej aplikacji

5 min czytania
W tej lekcji5

W poprzedniej lekcji widziałeś jak jeden wadliwy moduł gasi całą stację, a try/catch wokół JSX niczego nie ratuje. Potrzebujemy mechanizmu, który działa tam, gdzie React naprawdę renderuje dzieci. Error Boundaries (granice błędów) to specjalne komponenty React, które działają jak systemy bezpieczeństwa na stacji kosmicznej. Gdy jeden moduł ulegnie awarii, Error Boundary izoluje problem i zapobiega zniszczeniu całej stacji, dokładnie tak jak grodź zamyka rozszczelniony przedział.

Czym są Error Boundaries?

Error Boundary to komponent klasowy React, który przechwytuje błędy JavaScript w całym drzewie komponentów potomnych, loguje je i wyświetla zastępczy UI zamiast drzewa, które uległo awarii. Dlaczego klasowy? Dokumentacja react.dev mówi jasno: obecnie nie da się napisać Error Boundary jako komponentu funkcyjnego, bo nie ma hooka odpowiadającego metodom poniżej. Na szczęście taki komponent piszesz raz i używasz w całej flocie.

Dwie kluczowe metody

Error Boundary to komponent klasowy, który implementuje jedną lub obie z tych metod:

  1. static getDerivedStateFromError(error) - Aktualizuje stan, aby następny render pokazał zastępczy UI
  2. componentDidCatch(error, errorInfo) - Loguje informację o błędzie (np. do serwisu monitoringu)

Kolejność ma znaczenie. Gdy dziecko rzuci błąd, React najpierw wywołuje getDerivedStateFromError w fazie renderowania, potem wywołuje render(), który już pokazuje fallback, a dopiero po zatwierdzeniu zmian na ekranie wywołuje componentDidCatch. Dlatego pierwsza metoda musi być czysta (tylko zwraca nowy stan), a efekty uboczne, takie jak wysłanie raportu, należą do drugiej.

Podstawowa implementacja

Poniżej pełna, minimalna grodź. Stan hasError mówi, czy przedział jest rozszczelniony, a error przechowuje sam błąd, żeby pokazać jego treść:

1import React from 'react';
2
3class ErrorBoundary extends React.Component {
4  constructor(props) {
5    super(props);
6    this.state = { hasError: false, error: null };
7  }
8
9  // Wywoływana gdy komponent potomny rzuci błąd
10  // Zwraca obiekt aktualizujący stan
11  static getDerivedStateFromError(error) {
12    return { hasError: true, error: error };
13  }
14
15  // Wywoływana po przechwyceniu błędu
16  // Świetne miejsce na logowanie błędów
17  componentDidCatch(error, errorInfo) {
18    console.error('Error caught:', error);
19    console.error('Component stack:', errorInfo.componentStack);
20  }
21
22  render() {
23    if (this.state.hasError) {
24      return (
25        <div className="error-fallback">
26          <h2>Coś poszło nie tak!</h2>
27          <p>{this.state.error?.message}</p>
28        </div>
29      );
30    }
31
32    return this.props.children;
33  }
34}

Dopóki nic się nie psuje, render() po prostu zwraca this.props.children, więc granica jest niewidoczna i nie zmienia wyglądu ani działania dzieci. Pole errorInfo.componentStack to ścieżka komponentów prowadząca do awarii, bezcenna przy szukaniu przyczyny.

Używanie Error Boundaries

Error Boundary opakowuje komponenty, które chcemy chronić. Każdy moduł stacji dostaje własną grodź:

1function SpaceStation() {
2  return (
3    <div className="station">
4      <ErrorBoundary>
5        <Navigation />
6      </ErrorBoundary>
7
8      <ErrorBoundary>
9        <MainPanel />
10      </ErrorBoundary>
11
12      <ErrorBoundary>
13        <StatusBar />
14      </ErrorBoundary>
15    </div>
16  );
17}

Teraz jeśli MainPanel rzuci błąd, Navigation i StatusBar dalej będą działać! Granica łapie błędy tylko ze swojego poddrzewa, więc sąsiednie moduły w ogóle nie dowiadują się o awarii.

Granularność Error Boundaries

Możesz użyć różnych poziomów granularności. Dobrze jest myśleć o nich jak o warstwach osłon statku.

Poziom aplikacji (ogólna siatka bezpieczeństwa)

Najszersza granica otacza całą aplikację i łapie wszystko, czego nie złapały węższe:

1function App() {
2  return (
3    <ErrorBoundary fallback={<h1>Awaria całej stacji!</h1>}>
4      <SpaceStation />
5    </ErrorBoundary>
6  );
7}

Ta granica nie ochroni sąsiadów, bo zastępuje fallbackiem wszystko, ale zamienia biały ekran na czytelny komunikat. Prop fallback obsłużymy w naszej klasie za chwilę.

Poziom sekcji (izolacja modułów)

Węższe granice otaczają większe sekcje, każda z własnym komunikatem:

1function SpaceStation() {
2  return (
3    <div>
4      <ErrorBoundary fallback={<p>Nawigacja offline</p>}>
5        <Navigation />
6      </ErrorBoundary>
7      <ErrorBoundary fallback={<p>Panel sterowania niedostępny</p>}>
8        <ControlPanel />
9      </ErrorBoundary>
10    </div>
11  );
12}

Awaria panelu sterowania wyłącza tylko jego przedział, a nawigacja dalej prowadzi statek.

Poziom komponentu (precyzyjna izolacja)

Najdrobniejsza granica chroni pojedyncze widgety, na przykład każdy kafelek na pulpicie:

1function Dashboard() {
2  return (
3    <div className="dashboard">
4      {widgets.map(widget => (
5        <ErrorBoundary key={widget.id} fallback={<WidgetError />}>
6          <Widget data={widget} />
7        </ErrorBoundary>
8      ))}
9    </div>
10  );
11}

Zwróć uwagę, że key trafia na ErrorBoundary, bo to on jest teraz bezpośrednim elementem listy. Moja rada: zacznij od granicy na poziomie aplikacji i dokładaj węższe tam, gdzie awaria jest prawdopodobna, na przykład przy danych z zewnętrznych API.

Co Error Boundaries NIE przechwytują

Ważne jest wiedzieć, że Error Boundaries nie przechwytują błędów w:

  1. Event handlerach - używaj try/catch
  2. Kodzie asynchronicznym - setTimeout, fetch, Promise
  3. Renderowaniu po stronie serwera (SSR)
  4. Błędach w samym Error Boundary (a nie w jego dzieciach)

Wyjątek od punktu 2 pojawił się w React 19: błędy rzucone wewnątrz funkcji przekazanej do startTransition z hooka useTransition trafiają do najbliższej granicy. W zwykłym handlerze nadal potrzebujesz try/catch:

1// Event handler - wymaga try/catch
2function Button() {
3  const handleClick = () => {
4    try {
5      riskyOperation();
6    } catch (err) {
7      // Obsłuż błąd tutaj
8    }
9  };
10  return <button onClick={handleClick}>Klik</button>;
11}

Granica nad tym przyciskiem nie zareaguje na błąd z handleClick, bo w chwili kliknięcia React niczego nie renderuje. Obsługa należy do handlera.

Customowy fallback z propem

Najlepsza praktyka to tworzenie reużywalnego Error Boundary z konfigurowalnym fallbackiem. Ta wersja sprawdza, czy rodzic przekazał prop fallback, i dopiero gdy go brak, pokazuje domyślny komunikat:

1class ErrorBoundary extends React.Component {
2  constructor(props) {
3    super(props);
4    this.state = { hasError: false, error: null };
5  }
6
7  static getDerivedStateFromError(error) {
8    return { hasError: true, error };
9  }
10
11  componentDidCatch(error, info) {
12    console.error('Error boundary caught:', error, info);
13  }
14
15  render() {
16    if (this.state.hasError) {
17      // Używamy fallback prop jeśli jest dostępny
18      if (this.props.fallback) {
19        return this.props.fallback;
20      }
21      return <h2>Wystąpił błąd</h2>;
22    }
23    return this.props.children;
24  }
25}
26
27// Użycie z customowym fallbackiem
28<ErrorBoundary fallback={<p>Moduł uszkodzony, ale stacja działa!</p>}>
29  <RiskyModule />
30</ErrorBoundary>

Logika łapania się nie zmieniła, zmieniło się tylko to, co widzi załoga. W następnej lekcji zobaczysz bibliotekę react-error-boundary, która dodaje do takiej klasy reset, automatyczne czyszczenie i hook do błędów asynchronicznych.

Pamiętaj: Error Boundary to grodź awaryjna, która zamyka jeden przedział, żeby reszta statku mogła lecieć dalej.

Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2import './styles.css';
3
4// Error Boundary - komponent klasowy
5class ErrorBoundary extends React.Component {
6  constructor(props) {
7    super(props);
8    this.state = { hasError: false, error: null };
9  }
10
11  static getDerivedStateFromError(error) {
12    return { hasError: true, error };
13  }
14
15  componentDidCatch(error, errorInfo) {
16    console.error('ErrorBoundary caught:', error);
17    console.error('Component stack:', errorInfo.componentStack);
18  }
19
20  render() {
21    if (this.state.hasError) {
22      return (
23        <div className="error-fallback">
24          <h3>Modul uszkodzony</h3>
25          <p>{this.state.error?.message}</p>
26          <button
27            onClick={() => this.setState({ hasError: false })}
28            className="retry-btn"
29          >
30            Restart modulu
31          </button>
32        </div>
33      );
34    }
35    return this.props.children;
36  }
37}
38
39// Komponent ktory moze rzucic blad
40function UnstableModule({ name, failRate = 0.5 }) {
41  if (Math.random() < failRate) {
42    throw new Error(name + ' - awaria systemu!');
43  }
44  return (
45    <div className="module-ok">
46      <span className="status-dot green" />
47      {name}: Operacyjny
48    </div>
49  );
50}
51
52// Stabilny komponent
53function StableModule({ name }) {
54  return (
55    <div className="module-ok">
56      <span className="status-dot green" />
57      {name}: Operacyjny
58    </div>
59  );
60}
61
62export default function App() {
63  const [key, setKey] = useState(0);
64
65  return (
66    <div className="app">
67      <h1>Error Boundaries</h1>
68      <p className="subtitle">
69        Kazdy modul jest chroniony osobnym Error Boundary
70      </p>
71      <button
72        onClick={() => setKey(k => k + 1)}
73        className="reset-btn"
74      >
75        Zresetuj stacje (key: {key})
76      </button>
77
78      <div className="station" key={key}>
79        <ErrorBoundary>
80          <UnstableModule name="Silnik glowny" failRate={0.4} />
81        </ErrorBoundary>
82
83        <ErrorBoundary>
84          <UnstableModule name="Komunikacja" failRate={0.4} />
85        </ErrorBoundary>
86
87        <ErrorBoundary>
88          <StableModule name="Systemy podtrzymywania" />
89        </ErrorBoundary>
90
91        <ErrorBoundary>
92          <UnstableModule name="Sensory" failRate={0.4} />
93        </ErrorBoundary>
94      </div>
95    </div>
96  );
97}

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Co robi metoda static getDerivedStateFromError(error) w Error Boundary?

  2. 2. Których błędów Error Boundary NIE przechwytuje?

Zadania praktyczne w grze

  • Klikanie w kolejności

    Kliknij w kolejności etapy działania Error Boundary po wystąpieniu błędu:

  • Edytor kodu

    Zaimplementuj Error Boundary

  • Klikanie w kolejności

    Kliknij w kolejności elementy potrzebne do stworzenia Error Boundary:

  • Edytor kodu

    Zaimplementuj customowy Error Boundary

  • Układanie w pionie

    Uporządkuj poziomy granularności Error Boundaries od najszerszego do najwęższego:

Przydatne artykuły