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

Zaawansowane wzorce Error Handling

6 min czytania
W tej lekcji5

Twoje granice błędów łapią awarie i pokazują fallback, ale zastanów się: skąd dowiesz się, że o trzeciej w nocy tysiąc osób zobaczyło komunikat "Coś poszło nie tak"? Użytkownik nie wyśle Ci konsoli, a Ty nie siedzisz przy każdej przeglądarce. W tej lekcji poznamy zaawansowane techniki obsługi błędów, które możesz zastosować w złożonych aplikacjach React. Te wzorce są jak zaawansowane systemy bezpieczeństwa na stacji kosmicznej - wielowarstwowe, inteligentne i elastyczne.

Error Monitoring i Logging

W produkcji musisz wiedzieć o błędach natychmiast. Zintegruj Error Boundary z systemem monitoringu. Poniższa klasa robi trzy rzeczy: generuje identyfikator awarii, wysyła raport do serwisu (funkcja logErrorToService to Twój klient, na przykład Sentry) i pokazuje użytkownikowi kod, który może podać wsparciu:

1class MonitoredErrorBoundary extends React.Component {
2  constructor(props) {
3    super(props);
4    this.state = { hasError: false, error: null, errorId: null };
5  }
6
7  static getDerivedStateFromError(error) {
8    return { hasError: true, error };
9  }
10
11  componentDidCatch(error, errorInfo) {
12    // Generuj unikalny ID błędu
13    const errorId = Date.now().toString(36);
14    this.setState({ errorId });
15
16    // Wyślij do serwisu monitoringu
17    logErrorToService({
18      errorId,
19      message: error.message,
20      stack: error.stack,
21      componentStack: errorInfo.componentStack,
22      timestamp: new Date().toISOString(),
23      url: window.location.href,
24      userAgent: navigator.userAgent
25    });
26  }
27
28  render() {
29    if (this.state.hasError) {
30      return (
31        <div className="error-report">
32          <h3>Wystąpił błąd</h3>
33          <p>Kod błędu: {this.state.errorId}</p>
34          <p>Nasz zespół został powiadomiony.</p>
35          <button onClick={() => this.setState({ hasError: false })}>
36            Spróbuj ponownie
37          </button>
38        </div>
39      );
40    }
41    return this.props.children;
42  }
43}

Raport wysyłamy w componentDidCatch, a nie w getDerivedStateFromError, bo ta druga działa w fazie renderowania i musi być czysta. Date.now().toString(36) to tylko prosty identyfikator oparty na czasie, a dwie awarie w tej samej milisekundzie dostałyby ten sam kod. W produkcji użyj crypto.randomUUID() albo identyfikatora zwróconego przez serwis monitoringu.

Context-aware Error Handling

Różne części aplikacji mogą wymagać różnej obsługi błędów. Awaria systemu podtrzymywania życia to alarm, awaria widżetu pogody to wpis w dzienniku. Kontekst React (createContext i useContext) pozwala przekazać tę politykę całemu poddrzewu bez przekazywania propsów:

1import { createContext, useContext } from 'react';
2
3const ErrorContext = createContext({
4  onError: (error) => console.error(error),
5  severity: 'normal'
6});
7
8function useErrorHandler() {
9  const { onError, severity } = useContext(ErrorContext);
10
11  return (error) => {
12    if (severity === 'critical') {
13      // Dla krytycznych sekcji - wyślij alert
14      sendAlert(error);
15    }
16    onError(error);
17  };
18}
19
20// Użycie
21function CriticalSection({ children }) {
22  return (
23    <ErrorContext.Provider value={{
24      onError: (err) => sendToMonitoring(err),
25      severity: 'critical'
26    }}>
27      <ErrorBoundary>
28        {children}
29      </ErrorBoundary>
30    </ErrorContext.Provider>
31  );
32}

Każdy komponent wewnątrz CriticalSection wywoła useErrorHandler() i dostanie funkcję, która sama wie, czy podnieść alarm. W React 19 możesz też pisać krócej <ErrorContext value={...}>, zapis z .Provider nadal działa.

Error Recovery Strategies

Samo pokazanie błędu to dopiero początek. Poniżej dwie strategie odzyskiwania.

Strategy 1: Automatyczne odświeżanie komponentu

Ta strategia korzysta z ErrorBoundary z biblioteki react-error-boundary i ze sztuczki z propem key. Gdy key się zmienia, React niszczy stary komponent i tworzy nowy, z czystym stanem:

1function AutoRecoveryBoundary({ children, maxRetries = 3 }) {
2  const [retryCount, setRetryCount] = useState(0);
3  const [key, setKey] = useState(0);
4
5  const handleReset = () => {
6    if (retryCount < maxRetries) {
7      setRetryCount(prev => prev + 1);
8      setKey(prev => prev + 1); // Wymusza remount
9    }
10  };
11
12  return (
13    <ErrorBoundary
14      key={key}
15      FallbackComponent={({ error }) => (
16        <div>
17          <p>Błąd: {error.message}</p>
18          {retryCount < maxRetries ? (
19            <button onClick={handleReset}>
20              Spróbuj ponownie ({maxRetries - retryCount} prób pozostało)
21            </button>
22          ) : (
23            <p>Nie udało się naprawić błędu. Skontaktuj się z supportem.</p>
24          )}
25        </div>
26      )}
27    >
28      {children}
29    </ErrorBoundary>
30  );
31}

Licznik retryCount żyje poza granicą, więc remount go nie zeruje, i dlatego limit prób działa. Wszystko wewnątrz granicy zaczyna od zera.

Strategy 2: Fallback do prostszej wersji

Zamiast komunikatu możesz pokazać uboższą, ale działającą wersję tej samej informacji. Interaktywny wykres zależy od ciężkiej biblioteki, zwykła tabela nie:

1function ChartWithFallback({ data }) {
2  return (
3    <ErrorBoundary
4      FallbackComponent={() => (
5        // Zamiast interaktywnego wykresu, pokaż prostą tabelę
6        <table>
7          <thead><tr><th>Nazwa</th><th>Wartość</th></tr></thead>
8          <tbody>
9            {data.map(item => (
10              <tr key={item.id}>
11                <td>{item.name}</td>
12                <td>{item.value}</td>
13              </tr>
14            ))}
15          </tbody>
16        </table>
17      )}
18    >
19      <InteractiveChart data={data} />
20    </ErrorBoundary>
21  );
22}

Dane są te same, zmienia się tylko sposób ich pokazania. Załoga dalej widzi odczyty, choć bez animacji.

Global Error Handling

Przechwytuj nieobsłużone błędy na poziomie globalnym. Zdarzenie error na window łapie wyjątki, których nikt nie złapał, na przykład z handlerów i timerów, a unhandledrejection łapie odrzucone obietnice bez .catch():

1// W głównym komponencie aplikacji (np. App)
2useEffect(() => {
3  const handleUnhandledError = (event) => {
4    console.error('Unhandled error:', event.error);
5    // Wyślij do monitoringu
6  };
7
8  const handleUnhandledRejection = (event) => {
9    console.error('Unhandled promise rejection:', event.reason);
10    // Wyślij do monitoringu
11  };
12
13  window.addEventListener('error', handleUnhandledError);
14  window.addEventListener('unhandledrejection', handleUnhandledRejection);
15
16  return () => {
17    window.removeEventListener('error', handleUnhandledError);
18    window.removeEventListener('unhandledrejection', handleUnhandledRejection);
19  };
20}, []);

Funkcja sprzątająca zdejmuje oba nasłuchiwacze przy odmontowaniu, więc nie dublują się. Pamiętaj, że według react.dev w trybie deweloperskim błędy złapane przez componentDidCatch też docierają do window, a w produkcji już nie, więc w dev zobaczysz niektóre błędy dwa razy.

React 19 dodał do tego drugą warstwę: opcje onCaughtError i onUncaughtError funkcji createRoot. Pierwsza dostaje błędy złapane przez dowolną granicę, druga te, których nie złapała żadna:

1import { createRoot } from 'react-dom/client';
2
3const root = createRoot(document.getElementById('root'), {
4  onCaughtError: (error, errorInfo) => {
5    // Błąd złapany przez Error Boundary
6    reportToMonitoring('caught', error, errorInfo.componentStack);
7  },
8  onUncaughtError: (error, errorInfo) => {
9    // Błąd, którego nie złapała żadna granica
10    reportToMonitoring('uncaught', error, errorInfo.componentStack);
11  }
12});
13root.render(<App />);

Oba callbacki dostają componentStack, więc masz jedno centralne miejsce na raportowanie wszystkich błędów renderowania. Domyślnie React po prostu loguje je do konsoli.

Podsumowanie wzorców

Zbierzmy wszystkie warstwy osłon w jednej tabeli:

WzorzecKiedy używać
Error BoundaryBłędy renderowania komponentów
try/catchEvent handlery i kod synchroniczny
useErrorBoundaryBłędy asynchroniczne w Error Boundary
RetryNiestabilne połączenia sieciowe
Fallback contentOpcjonalne dane
Global handlerNieprzewidziane błędy

Moja rada: w nowym projekcie React 19 zacznij od onCaughtError i onUncaughtError w createRoot, bo to najtańszy sposób, żeby żaden błąd renderowania nie przeszedł niezauważony. W projekcie głównym tej lokacji połączysz wszystkie te wzorce w jeden odporny pulpit misji.

Pamiętaj: dobra stacja nie tylko przetrwa awarię, ale też od razu zgłasza ją do centrum kontroli misji.

Kod do tej lekcji: App.jsx
1import React, { useState, useEffect, createContext, useContext } from 'react';
2import './styles.css';
3
4// Error Logging Context
5const ErrorLogContext = createContext({ logs: [], addLog: () => {} });
6
7function useErrorLog() {
8  return useContext(ErrorLogContext);
9}
10
11// Monitored Error Boundary
12class MonitoredBoundary extends React.Component {
13  constructor(props) {
14    super(props);
15    this.state = { hasError: false, error: null, retries: 0 };
16  }
17  static getDerivedStateFromError(error) {
18    return { hasError: true, error };
19  }
20  componentDidCatch(error, info) {
21    const log = {
22      id: Date.now(),
23      section: this.props.section || 'unknown',
24      message: error.message,
25      time: new Date().toLocaleTimeString(),
26    };
27    if (this.props.onLogError) {
28      this.props.onLogError(log);
29    }
30  }
31  handleRetry = () => {
32    if (this.state.retries < (this.props.maxRetries || 3)) {
33      this.setState(s => ({
34        hasError: false,
35        error: null,
36        retries: s.retries + 1,
37      }));
38    }
39  };
40  render() {
41    if (this.state.hasError) {
42      const remaining = (this.props.maxRetries || 3) - this.state.retries;
43      return (
44        <div className="monitored-error">
45          <h4>{this.props.section} - Awaria</h4>
46          <p>{this.state.error.message}</p>
47          {remaining > 0 ? (
48            <button onClick={this.handleRetry} className="retry-btn">
49              Ponow ({remaining} prob)
50            </button>
51          ) : (
52            <p className="no-retry">Brak prob - zglos blad</p>
53          )}
54        </div>
55      );
56    }
57    return this.props.children;
58  }
59}
60
61function UnstableWidget({ name, fail = 0.5 }) {
62  if (Math.random() < fail) {
63    throw new Error(name + ': system failure');
64  }
65  return (
66    <div className="widget-ok">
67      <span className="dot" /> {name}: Online
68    </div>
69  );
70}
71
72// Error Log Panel
73function ErrorLogPanel({ logs }) {
74  if (logs.length === 0) return (
75    <div className="log-panel">
76      <h3>Dziennik bledow</h3>
77      <p className="empty">Brak zarejestrowanych bledow</p>
78    </div>
79  );
80  return (
81    <div className="log-panel">
82      <h3>Dziennik bledow ({logs.length})</h3>
83      {logs.map(log => (
84        <div key={log.id} className="log-entry">
85          <span className="log-time">{log.time}</span>
86          <span className="log-section">[{log.section}]</span>
87          <span className="log-msg">{log.message}</span>
88        </div>
89      ))}
90    </div>
91  );
92}
93
94export default function App() {
95  const [key, setKey] = useState(0);
96  const [logs, setLogs] = useState([]);
97
98  const addLog = (log) => {
99    setLogs(prev => [log, ...prev].slice(0, 10));
100  };
101
102  return (
103    <div className="app">
104      <h1>Error Monitoring</h1>
105      <button onClick={() => setKey(k => k + 1)} className="reset-btn">
106        Restart systemow
107      </button>
108
109      <div className="grid" key={key}>
110        <MonitoredBoundary section="Silniki" maxRetries={3} onLogError={addLog}>
111          <UnstableWidget name="Silniki" fail={0.5} />
112        </MonitoredBoundary>
113
114        <MonitoredBoundary section="Nawigacja" maxRetries={3} onLogError={addLog}>
115          <UnstableWidget name="Nawigacja" fail={0.5} />
116        </MonitoredBoundary>
117
118        <MonitoredBoundary section="Lacznosc" maxRetries={3} onLogError={addLog}>
119          <UnstableWidget name="Lacznosc" fail={0.5} />
120        </MonitoredBoundary>
121
122        <MonitoredBoundary section="Sensory" maxRetries={3} onLogError={addLog}>
123          <UnstableWidget name="Sensory" fail={0.5} />
124        </MonitoredBoundary>
125      </div>
126
127      <ErrorLogPanel logs={logs} />
128    </div>
129  );
130}

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. Która metoda Error Boundary jest najlepsza do logowania błędów do zewnętrznego serwisu?

  2. 2. Na czym polega strategia 'fallback do prostszej wersji komponentu'?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Układanie w pionie

    Uporządkuj strategie naprawy błędów od najprostszej do najbardziej złożonej:

  • Edytor kodu

    Zaimplementuj RetryErrorBoundary z maxRetries

  • Układanie w poziomie

    Ułóż składnię dodania globalnego handlera błędów:

  • Układanie w pionie

    Uporządkuj kroki budowania resilient aplikacji:

  • Edytor kodu

    Zaimplementuj taby z lazy loading

  • Układanie w pionie

    Uporządkuj elementy strony według priorytetu ładowania (od najwyższego):

  • Układanie w poziomie

    Ułóż składnię ErrorBoundary z resetKeys:

  • Edytor kodu

    Zbuduj mini resilient dashboard

  • Układanie w pionie

    Uporządkuj warstwy odporności od zewnętrznej do wewnętrznej:

Przydatne artykuły