Paweł Łukasiewicz
2026-02-03
Paweł Łukasiewicz
2026-02-03
Wprowadzenie
Aplikacja działa świetnie... do momentu gdy użytkownik wprowadzi nieoczekiwane dane, API zwróci błąd, lub nastąpi nieprzewidziany błąd. Wtedy biały ekran śmierci. Aplikacja przestaje działać. Użytkownik widzi pustą stronę. Koniec doświadczenia.
🔍 Co to jest "biały ekran śmierci"?
White Screen of Death (WSOD) - gdy aplikacja React napotka nieobsłużony błąd, całkowicie przestaje działać. Użytkownik widzi:
Pustą białą stronę (nic się nie renderuje)
Lub w development mode: czerwony ekran z błędem
Dlaczego React tak robi? Bo woli pokazać nic, niż pokazać zepsutą aplikację która może wprowadzać użytkownika w błąd lub źle działać.
Problem: Dla użytkownika to katastrofa - traci całą pracę, nie wie co się stało, nie może nic zrobić. 😱
Błędy są nieuniknione – API pada, dane są niepoprawne, użytkownicy robią rzeczy których nie przewidzieliśmy. Profesjonalna aplikacja nie może po prostu przestać działać. Musi gracefully obsłużyć błąd: pokazać przyjazny komunikat, zalogować błąd do monitoringu, pozwolić użytkownikowi kontynuować pracę.
🔍 Co znaczy "gracefully" obsłużyć błąd?
Graceful error handling = obsługa błędu w elegancki sposób:
✅ Pokazujesz przyjazny komunikat zamiast białego ekranu
✅ Logujesz błąd żeby deweloperzy mogli go naprawić
✅ Reszta aplikacji NADAL działa (np. nawigacja, menu)
✅ Dajesz użytkownikowi opcje: "Spróbuj ponownie", "Wróć do strony głównej"
Przykład z życia: YouTube - gdy film nie załaduje się, widzisz komunikat "Wystąpił błąd" i przycisk "Spróbuj ponownie". Reszta strony (menu, sidebar, inne filmy) DZIAŁA. To jest graceful! 🎯
W tym wpisie nauczymy się profesjonalnie obsługiwać błędy w React . Poznamy Error Boundaries , nauczymy się tworzyć fallback UI, zintegrujemy z systemami monitoringu (Sentry ), zbudujemy globalne error handling. To będzie praktyczny wpis – po nim Twoja aplikacja będzie odporna na błędy.
Problem z błędami w React
🔍 Typowe scenariusze błędów:
API zwraca null zamiast obiektu → próbujesz odczytać data.name → błąd
Użytkownik nie ma internetu → fetch failuje → dane są undefined
Literówka w kodzie → users.lenght zamiast .length → błąd
Backend zmienił format danych → oczekujesz obiektu, dostajesz tablicę → błąd
function BrokenComponent() {
const [data, setData] = useState(null);
useEffect(() => {
// API zwraca null zamiast obiektu
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []);
// 💥 TypeError: Cannot read property 'name' of null
return <div>{data.name}</div>;
}
Rezultat: Biały ekran. Cała aplikacja przestaje działać.
🌟 Co się dokładnie dzieje krok po kroku:
Komponent się renderuje - data = null (wartość początkowa)
React próbuje wyrenderować JSX: <div>{data.name}</div>
Próbuje odczytać data.name → ale data to null!
💥 JavaScript rzuca błąd: "Cannot read property 'name' of null"
React łapie błąd i... odmontowuje CAŁĄ aplikację
Użytkownik widzi: Białą stronę 😱
Jak to naprawić? Dodaj sprawdzenie:
// ✅ Bezpieczna wersja
return <div>{data?.name || 'Loading...'}</div>;
// Lub
if (!data) return <div>Loading...</div>;
return <div>{data.name}</div>;
🔍 Dlaczego try-catch nie działa w React?
try-catch łapie TYLKO błędy synchroniczne w bloku kodu. NIE działa dla:
Renderowania TSX - dzieje się poza try-catch
Event handlers - wywołują się później, nie w try-catch
Async code - Promise/setTimeout wykonują się poza try-catch
Komponenty potomne - są w innym "miejscu" w kodzie
function ComponentWithTryCatch() {
try {
const data = null;
return <div>{data.name}</div>; // ❌ Try-catch nie łapie błędów w TSX!
} catch (error) {
return <div>Error occurred</div>;
}
}
🌟 Gdzie try-catch DZIAŁA vs NIE DZIAŁA:
// ✅ DZIAŁA - synchroniczny kod
function processData(data) {
try {
const result = JSON.parse(data);
return result;
} catch (error) {
console.error('Parse error:', error);
return null;
}
}
// ❌ NIE DZIAŁA - event handler
function BadButton() {
try {
return (
<button onClick={() => {
throw new Error('Boom!'); // Try-catch NIE łapie tego!
}}>
Click
</button>
);
} catch (error) {
return <div>Error</div>; // Nigdy się nie wykona
}
}
// ✅ DZIAŁA - try-catch WEWNĄTRZ handlera
function GoodButton() {
const handleClick = () => {
try {
throw new Error('Boom!');
} catch (error) {
console.error(error); // To zadziała!
}
};
return <button onClick={handleClick}>Click</button>;
}
// ❌ NIE DZIAŁA - komponent potomny
function Parent() {
try {
return <BrokenChild />; // Błąd w BrokenChild nie zostanie złapany
} catch (error) {
return <div>Error</div>;
}
}
💪 Ćwiczenie 1: Znajdź błędy
Które z poniższych błędów try-catch złapie?
function MyComponent() {
const [data, setData] = useState(null);
try {
// Błąd A: Synchroniczny
const x = JSON.parse('invalid json');
// Błąd B: Renderowanie
return <div>{data.name}</div>;
// Błąd C: Event handler
const handleClick = () => {
throw new Error('Click error');
};
// Błąd D: useEffect
useEffect(() => {
throw new Error('Effect error');
}, []);
} catch (error) {
return <div>Error: {error.message}</div>;
}
}
Odpowiedź: Tylko błąd A zostanie złapany! B, C, D nie.
Error Boundaries – rozwiązanie
Error Boundary to komponent, który łapie błędy TypeScript w całym drzewie potomnych komponentów , loguje je i wyświetla fallback UI.
🔍 Jak działa Error Boundary?
Error Boundary = specjalny komponent który działa jak "siatka bezpieczeństwa":
Owijasz nim część aplikacji:
<ErrorBoundary>
<UserProfile />
<UserPosts />
</ErrorBoundary>
Jeśli UserProfile lub UserPosts rzucą błąd: Error Boundary go łapie
Zamiast białego ekranu: pokazuje fallback UI (np. "Coś poszło nie tak")
Reszta aplikacji działa normalnie! (menu, sidebar, inne komponenty)
Analogia: To jak airbag w samochodzie - gdy jest wypadek (błąd), airbag (Error Boundary) chroni pasażera (użytkownika) przed największym uderzeniem (białym ekranem). 🛡️
⚠️ NIE łapią błędów w:
Event handlers (onClick, onChange)
Asynchronicznym kodzie (setTimeout, Promise)
Server-side rendering
Błędach w samym Error Boundary
✅ Łapią błędy w:
Renderowaniu komponentów potomnych
Lifecycle methods
Konstruktorach komponentów potomnych
🔍 Dlaczego Error Boundaries NIE łapią event handlers?
Event handlers (onClick, onChange) wykonują się POZA renderowaniem:
React renderuje komponent → wszystko OK
Użytkownik klika przycisk → onClick się wykonuje
onClick rzuca błąd → ale to już PO renderowaniu!
Error Boundary nie widzi tego błędu (bo działa tylko podczas renderowania)
Rozwiązanie: Użyj try-catch WEWNĄTRZ event handlera:
const handleClick = () => {
try {
// Twój kod który może rzucić błąd
doSomethingRisky();
} catch (error) {
console.error('Error in handler:', error);
setError(error); // Pokaż błąd użytkownikowi
}
};
⚠️ Ważne ograniczenia!
Error Boundary NIE jest "catch-all" dla wszystkich błędów. Musisz dodatkowo:
✅ Dodać try-catch w event handlerach
✅ Dodać .catch() do Promises
✅ Obsłużyć błędy w async functions
✅ Walidować dane zanim je użyjesz
Error Boundary to JEDNA warstwa obrony, nie jedyna! 🛡️
🌟 Przykład: Co Error Boundary łapie, a czego nie
function MyComponent() {
const [count, setCount] = useState(0);
// ✅ Error Boundary to ZŁAPIE
if (count > 5) {
throw new Error('Count too high!');
}
// ❌ Error Boundary tego NIE złapie
const handleClick = () => {
throw new Error('Click error');
};
// ❌ Error Boundary tego NIE złapie
useEffect(() => {
setTimeout(() => {
throw new Error('Timeout error');
}, 1000);
}, []);
// ❌ Error Boundary tego NIE złapie
const fetchData = async () => {
throw new Error('Async error');
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Click</button>
</div>
);
}
// Użycie z Error Boundary
<ErrorBoundary>
<MyComponent /> {/* Tylko błędy podczas renderowania zostaną złapane */}
</ErrorBoundary>
Tworzenie Error Boundary
Niestety, Error Boundaries mogą być tylko class components (na razie).
🔍 Dlaczego tylko class components?
React nie ma jeszcze hooków dla Error Boundaries. Potrzebujesz dwóch specjalnych lifecycle methods:
getDerivedStateFromError() - łapie błąd, aktualizuje state
componentDidCatch() - loguje błąd, wysyła do monitoringu
Te metody istnieją TYLKO w class components. Ale nie martw się - napiszesz Error Boundary RAZ, potem używasz wszędzie! 💪
// ErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null
};
}
static getDerivedStateFromError(error: Error): State {
// Aktualizuj state żeby następny render pokazał fallback UI
return {
hasError: true,
error
};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Loguj błąd do systemu monitoringu
console.error('Error caught by boundary:', error, errorInfo);
// Tutaj możesz wysłać do Sentry, LogRocket, etc.
}
render() {
if (this.state.hasError) {
// Możesz wyrenderować dowolny fallback UI
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>Coś poszło nie tak 😞</h2>
<p>Przepraszamy za niedogodności. Spróbuj odświeżyć stronę.</p>
<button onClick={() => window.location.reload()}>
Odśwież stronę
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
🔍 Jak to działa krok po kroku:
Normalny render: hasError = false → renderuje children (twoje komponenty)
Dziecko rzuca błąd: React wywołuje getDerivedStateFromError(error)
getDerivedStateFromError: ustawia hasError = true, zapisuje błąd
React wywołuje componentDidCatch: możesz zalogować błąd (Sentry, console)
Re-render: hasError = true → pokazuje fallback UI zamiast dzieci
Użytkownik widzi: Przyjazny komunikat zamiast białego ekranu! 🎉
function App() {
return (
<ErrorBoundary>
<MyComponent />
<AnotherComponent />
</ErrorBoundary>
);
}
Teraz jeśli MyComponent lub AnotherComponent rzuci błąd, Error Boundary go złapie i pokaże fallback UI.
🌟 Przykład: Custom fallback
// Własny komponent fallback
function CustomErrorFallback({ error }: { error: Error }) {
return (
<div style={{
padding: '40px',
background: '#fee',
border: '2px solid #f88',
borderRadius: '8px',
textAlign: 'center'
}}>
<h1>🚨 Ups! Coś poszło nie tak</h1>
<p>Nie martw się, już nad tym pracujemy!</p>
<details style={{ marginTop: '20px' }}>
<summary>Szczegóły techniczne</summary>
<pre>{error.message}</pre>
</details>
<button
onClick={() => window.location.href = '/'}
style={{
marginTop: '20px',
padding: '10px 20px',
fontSize: '16px',
cursor: 'pointer'
}}
>
Wróć do strony głównej
</button>
</div>
);
}
// Użycie
<ErrorBoundary fallback={<CustomErrorFallback error={new Error()} />}>
<MyComponent />
</ErrorBoundary>
React Error Boundary (biblioteka)
Dla funkcyjnych komponentów możemy użyć biblioteki:
🔍 Po co biblioteka react-error-boundary?
Biblioteka dodaje fajne features których nie ma w podstawowym Error Boundary:
✅ resetErrorBoundary - przycisk "Spróbuj ponownie" który resetuje błąd
✅ resetKeys - automatyczny reset gdy zmieni się prop (np. user ID)
✅ onReset callback - możesz zresetować state aplikacji
✅ useErrorHandler hook - łap async błędy
To oszczędza pisanie własnego boilerplate! 🎯
npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }: any) {
return (
<div role="alert">
<p>Coś poszło nie tak:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Spróbuj ponownie</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
// Loguj do Sentry
console.error('Logged error:', error, errorInfo);
}}
onReset={() => {
// Reset state aplikacji
}}
>
<MyComponent />
</ErrorBoundary>
);
}
🔍 Po co resetować Error Boundary?
Gdy błąd wystąpi, Error Boundary pokazuje fallback. Ale co jeśli użytkownik chce spróbować ponownie?
resetErrorBoundary() czyści błąd i próbuje ponownie renderować dzieci. Przydatne gdy:
Błąd był tymczasowy (np. network timeout)
Użytkownik poprawił dane i chce spróbować znowu
Chcesz dać szansę na naprawę bez odświeżania całej strony
import { ErrorBoundary, useErrorHandler } from 'react-error-boundary';
function MyComponent() {
const [count, setCount] = useState(0);
if (count === 5) {
throw new Error('Count reached 5!');
}
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// Reset count kiedy użytkownik kliknie "Try again"
}}
resetKeys={['someKey']} // Reset gdy klucz się zmieni
>
<MyComponent />
</ErrorBoundary>
);
}
🌟 Przykład: Reset z reset keys
function UserProfile({ userId }: { userId: string }) {
// Załaduj dane użytkownika
const user = useUser(userId); // Może rzucić błąd
return <div>{user.name}</div>;
}
function App() {
const [userId, setUserId] = useState('1');
return (
<div>
<select onChange={(e) => setUserId(e.target.value)}>
<option value="1">User 1</option>
<option value="2">User 2</option>
</select>
<ErrorBoundary
FallbackComponent={ErrorFallback}
resetKeys={[userId]} // ✨ Automatyczny reset gdy userId się zmieni!
>
<UserProfile userId={userId} />
</ErrorBoundary>
</div>
);
}
// Scenariusz:
// 1. userId=1 → błąd → pokazuje fallback
// 2. Użytkownik wybiera userId=2 → resetKeys=[2] (zmiana!)
// 3. Error Boundary automatycznie resetuje i próbuje ponownie!
// 4. Może zadziała dla userId=2! 🎉
💪 Ćwiczenie 2: Zbuduj Error Boundary z retry
Stwórz Error Boundary który:
Pokazuje liczbę prób (attempt 1, 2, 3...)
Ma przycisk "Spróbuj ponownie" (max 3 próby)
Po 3 próbach pokazuje "Skontaktuj się z supportem" i link do emaila
Loguje każdą próbę do console
Granularne Error Boundaries
Nie owiewaj całej aplikacji w jeden Error Boundary – użyj wielu!
🔍 Dlaczego wiele Error Boundaries?
Jeden globalny Error Boundary:
❌ Błąd w sidebar → CAŁA aplikacja przestaje działać
❌ Użytkownik nie wie gdzie był problem
❌ Traci dostęp do wszystkiego (nawet menu, nawigacji)
Wiele granularnych Error Boundaries:
✅ Błąd w sidebar → tylko sidebar pokazuje fallback
✅ Reszta aplikacji DZIAŁA NORMALNIE (MainContent, Header, Footer)
✅ Użytkownik może kontynuować pracę!
Analogia: To jak przepalony bezpiecznik w domu - wyłącza TYLKO pokój z problemem, nie cały dom! 💡
function App() {
return (
<div>
{/* Globalna Error Boundary */}
<ErrorBoundary fallback={<GlobalErrorFallback />}>
<Header />
{/* Error Boundary dla sidebar */}
<ErrorBoundary fallback={<SidebarErrorFallback />}>
<Sidebar />
</ErrorBoundary>
{/* Error Boundary dla głównej treści */}
<ErrorBoundary fallback={<ContentErrorFallback />}>
<MainContent />
</ErrorBoundary>
<Footer />
</ErrorBoundary>
</div>
);
}
Zalety:
Błąd w Sidebar nie crashuje MainContent
Lepsze UX – użytkownik może kontynuować pracę
Łatwiejsze debugowanie – wiesz gdzie błąd wystąpił
🌟 Przykład: E-commerce z granularnymi boundaries
function ShopApp() {
return (
<ErrorBoundary fallback={<AppCrashedFallback />}>
{/* Header zawsze działa */}
<Header />
{/* Koszyk - osobny boundary */}
<ErrorBoundary fallback={<div>Koszyk chwilowo niedostępny</div>}>
<ShoppingCart />
</ErrorBoundary>
{/* Filtry - osobny boundary */}
<ErrorBoundary fallback={<div>Filtry niedostępne</div>}>
<ProductFilters />
</ErrorBoundary>
{/* Lista produktów - osobny boundary */}
<ErrorBoundary fallback={<div>Nie można załadować produktów</div>}>
<ProductList />
</ErrorBoundary>
{/* Footer zawsze działa */}
<Footer />
</ErrorBoundary>
);
}
// Scenariusz:
// ❌ ProductList ma błąd → pokazuje "Nie można załadować produktów"
// ✅ Koszyk działa! Możesz dokończyć zakupy z tym co już masz
// ✅ Filtry działają! Możesz spróbować inne filtry
// ✅ Header/Footer działają! Możesz przejść na inną stronę
Użytkownik NIE traci dostępu do całej aplikacji! Może dokończyć zakupy! 🛒✨
Obsługa błędów async
Error Boundary NIE łapie błędów w async code:
⚠️ Częsty błąd początkujących!
Error Boundary łapie TYLKO błędy podczas renderowania . Async kod (fetch, setTimeout, Promises) wykonuje się PÓŹNIEJ, poza renderowaniem!
function AsyncComponent() {
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
throw new Error('Async error'); // ❌ Error Boundary NIE złapie!
});
}, []);
return <div>Content</div>;
}
🔍 Jak useErrorHandler działa?
useErrorHandler to hook który "przekazuje" async błąd do Error Boundary:
Tworzysz handleError = useErrorHandler()
W .catch() wywołujesz handleError(error)
useErrorHandler "rzuca" błąd w komponencie
Error Boundary łapie ten błąd! 🎯
import { useErrorHandler } from 'react-error-boundary';
function AsyncComponent() {
const handleError = useErrorHandler();
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
throw new Error('Async error');
})
.catch(handleError); // ✅ Przekaż błąd do Error Boundary
}, [handleError]);
return <div>Content</div>;
}
🔍 Jak działa custom useAsyncError hook?
Ten hook to sprytny trick:
Używa setState z funkcją która rzuca błąd
React wywołuje tę funkcję podczas aktualizacji stanu
Błąd rzucony podczas setState = błąd podczas renderowania!
Error Boundary łapie błędy podczas renderowania! 🎉
function useAsyncError() {
const [, setError] = useState();
return useCallback(
(error: Error) => {
setError(() => {
throw error; // Rzuć błąd w komponencie
});
},
[setError]
);
}
// Użycie
function AsyncComponent() {
const throwError = useAsyncError();
useEffect(() => {
fetch('/api/data')
.catch(throwError); // Rzuci błąd, Error Boundary złapie
}, [throwError]);
return <div>Content</div>;
}
🌟 Przykład: Kompletna obsługa async z loading/error
function UserData({ userId }: { userId: string }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const handleError = useErrorHandler();
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return res.json();
})
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setLoading(false);
handleError(error); // ✅ Error Boundary złapie
});
}, [userId, handleError]);
if (loading) return <div>Ładowanie...</div>;
if (!data) return null;
return <div>{data.name}</div>;
}
// Użycie z Error Boundary
<ErrorBoundary
FallbackComponent={({ error, resetErrorBoundary }) => (
<div>
<p>Nie udało się załadować danych użytkownika</p>
<button onClick={resetErrorBoundary}>Spróbuj ponownie</button>
</div>
)}
>
<UserData userId="123" />
</ErrorBoundary>
Integracja z Sentry
Sentry to najlepszy system monitoringu błędów dla aplikacji webowych.
🔍 Co to jest Sentry i po co?
Sentry to narzędzie które:
✅ Zbiera wszystkie błędy z produkcji (nie musisz czytać console.log użytkowników!)
✅ Pokazuje stack trace - dokładnie gdzie błąd wystąpił
✅ Informacje o użytkowniku - kto napotkał błąd, przeglądarka, system
✅ Częstotliwość - ile razy błąd wystąpił, u ilu użytkowników
✅ Alertowanie - email/Slack gdy pojawi się nowy błąd
Bez Sentry: Użytkownik widzi błąd → Ty nie wiesz o tym → Nie naprawisz
Z Sentry: Użytkownik widzi błąd → Sentry Cię powiadamia → Naprawiasz! 🎯
npm install @sentry/react
🔍 Parametry Sentry.init():
dsn - unikalny klucz Twojego projektu (dostaniesz z Sentry.io)
environment - 'development' lub 'production' (żeby odróżnić błędy)
tracesSampleRate - ile % requestów śledzić (1.0 = wszystkie, 0.1 = 10%)
replaysSessionSampleRate - ile % sesji nagrywać (darmowy plan: 0)
replaysOnErrorSampleRate - ile % sesji z błędem nagrywać (1.0 = wszystkie)
// main.tsx
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
environment: import.meta.env.MODE,
integrations: [
new Sentry.BrowserTracing(),
new Sentry.Replay()
],
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
import * as Sentry from '@sentry/react';
function App() {
return (
<Sentry.ErrorBoundary
fallback={({ error, resetError }) => (
<div>
<h2>Wystąpił błąd</h2>
<p>{error.message}</p>
<button onClick={resetError}>Spróbuj ponownie</button>
</div>
)}
showDialog // Pokazuje dialog do zgłoszenia błędu
>
<MyComponent />
</Sentry.ErrorBoundary>
);
}
🔍 Co to jest showDialog?
showDialog pokazuje użytkownikowi popup Sentry gdzie może:
Opisać co robił gdy błąd wystąpił
Podać swój email (opcjonalnie)
Wysłać raport do Ciebie
To BARDZO przydatne! Użytkownik może napisać: "Kliknąłem 'Zapisz' i wszystko się zepsuło" - wtedy łatwiej odtworzyć błąd! 💬
import * as Sentry from '@sentry/react';
function MyComponent() {
async function handleSubmit() {
try {
await submitForm();
} catch (error) {
// Zaloguj do Sentry z dodatkowym kontekstem
Sentry.captureException(error, {
tags: {
section: 'form-submission'
},
extra: {
formData: { /* ... */ }
}
});
// Pokaż error użytkownikowi
toast.error('Form submission failed');
}
}
return <form onSubmit={handleSubmit}>...</form>;
}
🔍 Tags vs Extra - jaka różnica?
Tags - krótkie wartości po których możesz filtrować w Sentry:
tags: {
section: 'checkout', // Możesz filtrować: "pokaż błędy z checkout"
userId: '123', // Możesz filtrować: "pokaż błędy użytkownika 123"
environment: 'prod'
}
Extra - dodatkowe dane do debugowania (dowolne):
extra: {
formData: { name: 'John', email: 'test@test.com' },
cartItems: [...],
userPreferences: {...}
}
🔍 Po co setUser?
Gdy znasz użytkownika (po logowaniu), powiedz o tym Sentry! Wtedy w raporcie błędu zobaczysz:
Email użytkownika (możesz go skontaktować!)
ID użytkownika (możesz zobaczyć jego dane w bazie)
Username (wiesz kto napotkał problem)
Super przydatne gdy chcesz odtworzyć błąd z konkretnymi danymi użytkownika! 🔍
import * as Sentry from '@sentry/react';
function App() {
const { user } = useAuth();
useEffect(() => {
if (user) {
Sentry.setUser({
id: user.id,
email: user.email,
username: user.name
});
} else {
Sentry.setUser(null);
}
}, [user]);
return <AppContent />;
}
🌟 Przykład: Pełna integracja Sentry
// sentry.ts
import * as Sentry from '@sentry/react';
export function initSentry() {
if (import.meta.env.PROD) {
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: 'production',
integrations: [
new Sentry.BrowserTracing(),
new Sentry.Replay({
maskAllText: true,
blockAllMedia: true,
}),
],
tracesSampleRate: 0.1, // 10% requestów
replaysSessionSampleRate: 0.01, // 1% sesji
replaysOnErrorSampleRate: 1.0, // 100% sesji z błędem
beforeSend(event, hint) {
// Nie wysyłaj błędów z development
if (event.environment === 'development') {
return null;
}
// Filtruj wrażliwe dane
if (event.request?.headers) {
delete event.request.headers['Authorization'];
}
return event;
},
});
}
}
// main.tsx
import { initSentry } from './sentry';
initSentry();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Obsługa błędów w event handlers
Error Boundaries NIE łapią błędów w event handlers:
function ButtonWithError() {
function handleClick() {
throw new Error('Error in handler'); // ❌ Error Boundary NIE złapie!
}
return <button onClick={handleClick}>Click</button>;
}
function SafeButton() {
const [error, setError] = useState<Error | null>(null);
function handleClick() {
try {
// Kod który może rzucić błąd
throw new Error('Something went wrong');
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
// Opcjonalnie zaloguj do Sentry
Sentry.captureException(err);
}
}
if (error) {
return <div>Error: {error.message}</div>;
}
return <button onClick={handleClick}>Click</button>;
}
🔍 Wrapper pattern - DRY dla error handling
Zamiast pisać try-catch w KAŻDYM handlerze, stwórz wrapper który robi to automatycznie:
// ❌ Powtarzalny kod
const handleClick = () => {
try { doSomething(); } catch (e) { console.error(e); }
};
const handleSubmit = () => {
try { submitForm(); } catch (e) { console.error(e); }
};
// ✅ Wrapper - pisz raz, używaj wszędzie
const handleClick = withErrorHandling(() => doSomething());
const handleSubmit = withErrorHandling(() => submitForm());
function withErrorHandling<T extends (...args: any[]) => any>(fn: T) {
return ((...args: Parameters<T>) => {
try {
return fn(...args);
} catch (error) {
console.error('Error in handler:', error);
Sentry.captureException(error);
toast.error('Operacja nie powiodła się');
}
}) as T;
}
// Użycie
function MyComponent() {
const handleClick = withErrorHandling(() => {
throw new Error('Boom!');
});
return <button onClick={handleClick}>Click</button>;
}
🌟 Przykład: Custom hook dla error handling
// useErrorHandler.ts
function useErrorHandler() {
const [error, setError] = useState<Error | null>(null);
const handleError = useCallback((error: Error) => {
setError(error);
Sentry.captureException(error);
toast.error(error.message || 'Wystąpił błąd');
}, []);
const clearError = useCallback(() => {
setError(null);
}, []);
return { error, handleError, clearError };
}
// Użycie
function MyForm() {
const { error, handleError, clearError } = useErrorHandler();
const handleSubmit = async (data: FormData) => {
try {
clearError();
await submitForm(data);
toast.success('Zapisano!');
} catch (err) {
handleError(err as Error);
}
};
return (
<form onSubmit={handleSubmit}>
{error && (
<div className="error-banner">
{error.message}
<button onClick={clearError}>×</button>
</div>
)}
{/* formularz */}
</form>
);
}
Fallback UI patterns
🔍 Dlaczego skeleton jako fallback?
Loading skeleton = szare prostokąty imitujące zawartość. Jako fallback:
✅ Zachowuje layout strony (nie "skacze")
✅ Wygląda znajomo (użytkownik widział przy loading)
✅ Subtelnie pokazuje że "coś jest nie tak"
Lepsze niż wielki czerwony komunikat który straszy użytkownika! 👻
function ErrorFallback({ error, resetErrorBoundary }: any) {
return (
<div className="error-container">
<div className="error-icon">⚠️</div>
<h2>Nie udało się załadować treści</h2>
<p className="error-message">{error.message}</p>
<div className="error-actions">
<button onClick={resetErrorBoundary} className="retry-button">
Spróbuj ponownie
</button>
<button onClick={() => window.location.href = '/'} className="home-button">
Wróć do strony głównej
</button>
</div>
</div>
);
}
function UserProfile({ userId }: { userId: number }) {
return (
<ErrorBoundary
fallback={
<div className="user-profile-error">
<div className="avatar-placeholder" />
<p>Nie udało się załadować profilu</p>
</div>
}
>
<UserProfileContent userId={userId} />
</ErrorBoundary>
);
}
🔍 Personalizowane komunikaty błędów
Nie każdy błąd jest taki sam! Lepiej pokazać:
Network error: "Sprawdź połączenie z internetem"
401 Unauthorized: "Zaloguj się ponownie"
404 Not Found: "Strona nie istnieje"
500 Server Error: "Problem z serwerem, spróbuj później"
Użytkownik wie CO zrobić! 🎯
interface CustomErrorBoundaryProps {
children: ReactNode;
}
function CustomErrorBoundary({ children }: CustomErrorBoundaryProps) {
return (
<ErrorBoundary
fallbackRender={({ error }) => {
// Network error
if (error.message.includes('fetch')) {
return <NetworkErrorFallback />;
}
// Authorization error
if (error.message.includes('401')) {
return <UnauthorizedFallback />;
}
// Not found
if (error.message.includes('404')) {
return <NotFoundFallback />;
}
// Generic error
return <GenericErrorFallback error={error} />;
}}
>
{children}
</ErrorBoundary>
);
}
🌟 Przykład: Różne fallbacki w praktyce
function NetworkErrorFallback() {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div style={{ fontSize: '48px' }}>📡</div>
<h2>Brak połączenia z internetem</h2>
<p>Sprawdź swoje połączenie i spróbuj ponownie</p>
<button onClick={() => window.location.reload()}>
Odśwież stronę
</button>
</div>
);
}
function UnauthorizedFallback() {
const navigate = useNavigate();
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div style={{ fontSize: '48px' }}>🔒</div>
<h2>Sesja wygasła</h2>
<p>Zaloguj się ponownie aby kontynuować</p>
<button onClick={() => navigate('/login')}>
Przejdź do logowania
</button>
</div>
);
}
function NotFoundFallback() {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div style={{ fontSize: '48px' }}>🔍</div>
<h2>Nie znaleziono strony</h2>
<p>Strona której szukasz nie istnieje lub została przeniesiona</p>
<button onClick={() => window.location.href = '/'}>
Wróć do strony głównej
</button>
</div>
);
}
💪 Ćwiczenie 3: Custom fallback components
Stwórz 3 fallback komponenty dla różnych scenariuszy:
PaymentErrorFallback - błąd płatności (pokaż numer support, email)
DataLoadErrorFallback - błąd ładowania danych (przycisk retry, loading skeleton)
PermissionErrorFallback - brak uprawnień (wyjaśnij dlaczego, link do upgrade)
Globalne error handling
🔍 Window error handlers - ostatnia linia obrony
Te handlery łapią błędy które "uciekły" Error Boundaries:
window.onerror - synchroniczne błędy TypeScript
unhandledrejection - Promise bez .catch()
To "siatka bezpieczeństwa dla siatki bezpieczeństwa" 🛡️🛡️
// main.tsx
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
Sentry.captureException(event.error);
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
Sentry.captureException(event.reason);
});
🔍 Interceptor - łap błędy API w jednym miejscu
Axios interceptor = middleware dla requestów. Łapie WSZYSTKIE błędy API:
✅ 500 Server Error → zaloguj do Sentry, pokaż toast
✅ 401 Unauthorized → wyloguj użytkownika, redirect do /login
✅ 404 Not Found → pokaż komunikat
Piszesz RAZ, działa dla WSZYSTKICH requestów! 🎯
// api/client.ts
import axios from 'axios';
import * as Sentry from '@sentry/react';
const apiClient = axios.create({
baseURL: '/api'
});
apiClient.interceptors.response.use(
(response) => response,
(error) => {
// Zaloguj błąd API
Sentry.captureException(error, {
tags: {
type: 'api-error',
status: error.response?.status
},
extra: {
url: error.config?.url,
method: error.config?.method
}
});
// Pokaż toast
if (error.response?.status === 500) {
toast.error('Błąd serwera. Spróbuj ponownie później.');
}
return Promise.reject(error);
}
);
🌟 Przykład: Kompleksowy axios interceptor
import axios from 'axios';
import { toast } from 'react-hot-toast';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
});
// Request interceptor (przed wysłaniem)
api.interceptors.request.use(
(config) => {
// Dodaj token
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor (po otrzymaniu odpowiedzi)
api.interceptors.response.use(
(response) => response,
(error) => {
const status = error.response?.status;
// 401 - Unauthorized
if (status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
toast.error('Sesja wygasła. Zaloguj się ponownie.');
}
// 403 - Forbidden
if (status === 403) {
toast.error('Nie masz uprawnień do tej operacji');
}
// 404 - Not Found
if (status === 404) {
toast.error('Zasób nie został znaleziony');
}
// 500 - Server Error
if (status === 500) {
Sentry.captureException(error);
toast.error('Błąd serwera. Spróbuj później.');
}
// Network Error
if (!error.response) {
toast.error('Brak połączenia z serwerem');
}
return Promise.reject(error);
}
);
export default api;
Praktyczny przykład: Kompletna obsługa błędów
// ErrorBoundary.tsx
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
import * as Sentry from '@sentry/react';
import { ReactNode } from 'react';
interface ErrorFallbackProps {
error: Error;
resetErrorBoundary: () => void;
}
function ErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) {
return (
<div className="error-fallback">
<div className="error-content">
<h2>Ups! Coś poszło nie tak</h2>
<p className="error-message">
Przepraszamy za niedogodności. Nasz zespół został powiadomiony o problemie.
</p>
{import.meta.env.DEV && (
<details className="error-details">
<summary>Szczegóły błędu (widoczne tylko w development)</summary>
<pre>{error.message}</pre>
<pre>{error.stack}</pre>
</details>
)}
<div className="error-actions">
<button onClick={resetErrorBoundary} className="btn-primary">
Spróbuj ponownie
</button>
<button
onClick={() => window.location.href = '/'}
className="btn-secondary"
>
Wróć do strony głównej
</button>
</div>
</div>
</div>
);
}
interface AppErrorBoundaryProps {
children: ReactNode;
}
export function AppErrorBoundary({ children }: AppErrorBoundaryProps) {
return (
<ReactErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
// Zaloguj do Sentry tylko w production
if (import.meta.env.PROD) {
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack
}
}
});
} else {
console.error('Error caught:', error, errorInfo);
}
}}
onReset={() => {
// Reset state aplikacji jeśli potrzeba
window.location.reload();
}}
>
{children}
</ReactErrorBoundary>
);
}
// App.tsx
function App() {
return (
<AppErrorBoundary>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/dashboard"
element={
<AppErrorBoundary>
<Dashboard />
</AppErrorBoundary>
}
/>
<Route
path="/profile"
element={
<AppErrorBoundary>
<Profile />
</AppErrorBoundary>
}
/>
</Routes>
</Router>
</AppErrorBoundary>
);
}
Testowanie Error Boundaries
🔍 Dlaczego testować Error Boundaries?
Error Boundary to KRYTYCZNY komponent - jeśli nie działa, użytkownik widzi biały ekran! Musisz mieć pewność że:
✅ Łapie błędy potomków
✅ Pokazuje fallback UI
✅ Wywołuje onError callback
✅ Reset działa poprawnie
// ErrorBoundary.test.tsx
import { render, screen } from '@testing-library/react';
import { describe, test, expect, vi } from 'vitest';
import { ErrorBoundary } from 'react-error-boundary';
function ThrowError({ shouldThrow }: { shouldThrow: boolean }) {
if (shouldThrow) {
throw new Error('Test error');
}
return <div>No error</div>;
}
describe('ErrorBoundary', () => {
test('renders children when no error', () => {
render(
<ErrorBoundary fallback={<div>Error occurred</div>}>
<ThrowError shouldThrow={false} />
</ErrorBoundary>
);
expect(screen.getByText('No error')).toBeInTheDocument();
});
test('renders fallback when error occurs', () => {
// Suppress console.error dla tego testu
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
render(
<ErrorBoundary fallback={<div>Error occurred</div>}>
<ThrowError shouldThrow={true} />
</ErrorBoundary>
);
expect(screen.getByText('Error occurred')).toBeInTheDocument();
spy.mockRestore();
});
test('calls onError when error occurs', () => {
const onError = vi.fn();
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
render(
<ErrorBoundary fallback={<div>Error</div>} onError={onError}>
<ThrowError shouldThrow={true} />
</ErrorBoundary>
);
expect(onError).toHaveBeenCalled();
spy.mockRestore();
});
});
🔍 Dlaczego mockujemy console.error?
React ZAWSZE loguje błędy do console.error (nawet gdy Error Boundary je łapie). W testach to "zaśmieca" output czerwonymi błędami.
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
// ... test ...
spy.mockRestore();
To wycisza console.error TYLKO dla tego testu! ✅
Best Practices
// ✅ Granularne boundaries
<ErrorBoundary>
<Header />
<ErrorBoundary>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary>
<MainContent />
</ErrorBoundary>
</ErrorBoundary>
// ❌ Jeden boundary dla wszystkiego
<ErrorBoundary>
<WholeApp />
</ErrorBoundary>
// ✅ Zawsze loguj w production
onError={(error) => {
if (import.meta.env.PROD) {
Sentry.captureException(error);
}
}}
// ❌ Techniczny komunikat
<div>Error: {error.stack}</div>
// ✅ User-friendly message
<div>
<h2>Coś poszło nie tak</h2>
<p>Przepraszamy za niedogodności. Spróbuj odświeżyć stronę.</p>
</div>
// ✅ Używaj useErrorHandler dla async
const handleError = useErrorHandler();
fetch('/api/data')
.catch(handleError);
// ✅ Daj użytkownikowi sposób na reset
<ErrorBoundary
onReset={() => {
// Reset state
}}
resetKeys={[userId]} // Auto-reset gdy się zmieni
>
Podsumowanie
To był ważny wpis o odporności aplikacji! Nauczyliśmy się:
✅ Problem – co się dzieje gdy błąd wystąpi
✅ Error Boundaries – jak łapać błędy w React
✅ Class vs biblioteka – własny Error Boundary vs react-error-boundary
✅ Granularne boundaries – wiele Error Boundaries w aplikacji
✅ Async errors – useErrorHandler dla Promise/fetch
✅ Sentry – integracja z systemem monitoringu
✅ Event handlers – obsługa błędów w onClick itp.
✅ Fallback UI – różne wzorce UI dla błędów
✅ Globalne handling – window.onerror, axios interceptors
✅ Kompletny przykład – AppErrorBoundary gotowy do użycia
✅ Testowanie – jak testować Error Boundaries
✅ 5 Best Practices – profesjonalne wzorce
Obsługa błędów to nie opcja – to konieczność w produkcyjnych aplikacjach. Teraz masz kompletną wiedzę by zbudować odporną aplikację!
W kolejnym wpisie poznamy komunikację z API – Axios vs Fetch, interceptory, error handling, upload plików!
Dodaj Error Boundaries do swojej aplikacji:
Globalny Error Boundary dla całej aplikacji
Granularne boundaries dla głównych sekcji
Integracja z Sentry (lub innym monitoringiem)
Custom error fallback UI
Obsługa async errors
(Bonus) Testy Error Boundaries
🎯 BONUS: Wielki projekt końcowy - Kompletny Error Handling System
Stwórz profesjonalny system obsługi błędów dla aplikacji e-commerce:
Komponenty:
GlobalErrorBoundary - owijające całą aplikację
RouteErrorBoundary - osobne dla każdej trasy
SectionErrorBoundary - dla Header, Sidebar, MainContent
Fallback UI:
NetworkErrorFallback (brak internetu)
PaymentErrorFallback (błąd płatności + support)
ProductLoadErrorFallback (nie załadowano produktów)
GenericErrorFallback (wszystko inne)
Monitoring:
Integracja z Sentry
User context (ID, email, username)
Custom tags (section, errorType, environment)
Breadcrumbs (historia akcji użytkownika)
Error Handlers:
Axios interceptor dla błędów API (401, 404, 500)
useErrorHandler hook dla async code
withErrorHandling wrapper dla event handlers
window.onerror + unhandledrejection
Testy:
Test że Error Boundary łapie błędy
Test że pokazuje poprawny fallback
Test że wywołuje onError
Test resetu Error Boundary
To profesjonalny production-ready error handling! Po ukończeniu tego projektu Twoja aplikacja będzie odporna jak czołg! 🛡️✨