Error Handling und Loading States in React
Error Handling und Loading States
~11 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026
Unser ProductListPage aus dem letzten Kapitel hat einen blinden Fleck: Schlägt fetchProducts() fehl (Netzwerkfehler, Server nicht erreichbar, falsche URL), landet der Fehler nur in der Browser-Konsole – der Nutzer sieht scheinbar für immer "Lade Produkte...". Das reparieren wir jetzt richtig.
Die drei Zustände jeder Datenladung
Jede API-Anfrage hat aus Sicht der Oberfläche immer genau drei mögliche Zustände, die Sie separat abbilden sollten:
- Laden – die Anfrage läuft noch, meist ein Spinner oder Platzhaltertext
- Fehler – die Anfrage ist fehlgeschlagen, meist eine Fehlermeldung plus "Erneut versuchen"-Button
- Erfolg – Daten sind da und werden angezeigt
Ein häufiger Anfängerfehler ist, nur EINEN ladend-State zu verwenden und Fehler nur zu loggen – dann gibt es effektiv keinen sichtbaren Unterschied zwischen "lädt noch" und "wird nie fertig, weil etwas kaputt ist".
ProductListPage.jsx: einen echten Fehlerzustand ergänzen
import { useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import ProductCard from '../components/ProductCard';
import { fetchProducts } from '../api/magentoApi';
function ProductListPage() {
const [produkte, setProdukte] = useState([]);
const [ladend, setLadend] = useState(true);
const [fehler, setFehler] = useState(null);
const [query, setQuery] = useState('');
const searchInputRef = useRef(null);
const navigate = useNavigate();
async function ladeProdukte() {
setLadend(true);
setFehler(null);
try {
const items = await fetchProducts();
setProdukte(items);
} catch (error) {
setFehler(error.message);
} finally {
setLadend(false);
}
}
useEffect(() => {
ladeProdukte();
}, []);
useEffect(() => {
if (!ladend && !fehler) {
searchInputRef.current?.focus();
}
}, [ladend, fehler]);
const gefiltert = produkte.filter((produkt) =>
produkt.name.toLowerCase().includes(query.toLowerCase())
);
const teuerstesProdukt = useMemo(() => {
return gefiltert.reduce(
(max, p) => (p.price > (max?.price ?? 0) ? p : max),
null
);
}, [gefiltert]);
if (ladend) {
return <p>Lade Produkte...</p>;
}
if (fehler) {
return (
<div>
<p>Produkte konnten nicht geladen werden: {fehler}</p>
<button onClick={ladeProdukte}>Erneut versuchen</button>
</div>
);
}
return (
<div>
<input
ref={searchInputRef}
type="text"
placeholder="Produkt suchen..."
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
{teuerstesProdukt && (
<p>Teuerstes Produkt: {teuerstesProdukt.name} ({teuerstesProdukt.price.toFixed(2)} €)</p>
)}
<div className="product-grid">
{gefiltert.map((produkt) => (
<ProductCard
key={produkt.sku}
name={produkt.name}
price={produkt.price}
imageUrl={produkt.imageUrl}
onSelect={() => navigate(`/products/${produkt.sku}`)}
/>
))}
</div>
</div>
);
}
export default ProductListPage;Drei wichtige Änderungen im Detail: Erstens, ladeProdukte() ist jetzt eine eigenständige Funktion (nicht mehr nur innerhalb von useEffect versteckt) – so kann der "Erneut versuchen"-Button sie direkt per onClick={{ladeProdukte}} erneut aufrufen. Zweitens, ein zweiter useEffect übernimmt das Autofokussieren des Suchfelds GENAU dann, wenn ladend UND fehler beide "aus" sind – das löst das Problem aus dem letzten Kapitel, dass das Feld beim ersten Rendern noch nicht existierte. Drittens, searchInputRef.current?.focus() mit Optional Chaining (?.) schützt zusätzlich davor, dass die Ref noch null ist, falls der Effekt aus irgendeinem Grund vor dem ersten Rendern läuft.
Tipp: Für unser Bewertungsformular (Kapitel 14–16) hatten wir bereits eine ähnliche, aber einfachere Fehlerbehandlung – dort ging es um FORMULAR-Validierungsfehler, hier um NETZWERK-Fehler. Beide folgen demselben Grundprinzip: Fehler als eigenen, sichtbaren State statt nur als Konsolen-Eintrag.