In modernen Webanwendungen arbeiten wir zunehmend mit externen Schnittstellen, wie z.B. APIs, Datenbanken oder Microservices. Diese Abhängigkeiten sind jedoch nicht immer zuverlässig. Netzwerkprobleme, Serverüberlastungen oder temporäre Timeouts können dazu führen, dass eine eigentlich funktionierende Operation fehlschlägt. Für solche Fälle gibt es ein bewährtes Konzept, das aus der Welt der verteilten Systeme stammt. Es handelt sich dabei um das Retry Pattern.
Das Retry Pattern beschreibt eine Strategie, mit der fehlgeschlagene Operationen automatisiert erneut ausgeführt werden. Je nach Grund für das Fehlschlagen wird hier oft mit einer kleinen Verzögerung zwischen den Versuchen gearbeitet. Ziel ist es, temporäre Fehler elegant abzufedern, ohne dass die gesamte Anwendung ins Stolpern gerät. In JavaScript – insbesondere im asynchronen Kontext von fetch()
, Datenbankoperationen oder WebSocket-Kommunikation – ist dieses Pattern ein echter Gamechanger.
Warum Retry überhaupt sinnvoll ist
Stell dir vor, du baust eine App, die auf eine REST-API zugreift. Bei jedem API-Call besteht eine geringe, aber reale Chance, dass ein Timeout oder ein 503-Fehler (Service Unavailable) auftritt. Diese Fehler bedeuten nicht zwingend, dass die API dauerhaft nicht erreichbar ist. Vielleicht ist nur ein kurzfristiger Aussetzer im Spiel.
Mit einem Retry Pattern kannst du solchen Fehlern vorbeugen. Statt bei der ersten Fehlermeldung aufzugeben, gibst du deiner Anwendung eine zweite oder dritte Chance. Dadurch steigen Zuverlässigkeit und Nutzererlebnis erheblich. Dies gilt besonders bei mobilen Clients oder instabilen Netzwerken.
Eine einfache Retry-Implementierung in JavaScript
Hier ein simples, aber wirkungsvolles Beispiel für ein Retry Pattern bei einem API-Call:
async function retry(fn, retries = 3, delay = 1000) {
try {
return await fn();
} catch (err) {
if (retries > 0) {
console.warn(`Fehlgeschlagen, versuche erneut... (${retries} verbleibend)`);
await new Promise(res => setTimeout(res, delay));
return retry(fn, retries - 1, delay);
} else {
throw err;
}
}
}
// Beispielverwendung
const fetchData = () => fetch('https://api.example.com/data');
retry(fetchData, 3, 2000)
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.error("Fehlgeschlagen nach mehreren Versuchen:", err));
Diese Funktion retry()
nimmt eine beliebige asynchrone Funktion entgegen und wiederholt sie bei Fehlern bis zu einer definierten Anzahl. Eine einfache setTimeout
-basierte Verzögerung sorgt dafür, dass nicht alle Versuche direkt hintereinander stattfinden.
Vorteile des Retry Patterns
Das Retry Pattern bietet zahlreiche Vorteile. Es erhöht die Robustheit deiner Anwendung und sorgt für eine bessere Nutzererfahrung, da temporäre Fehler automatisch abgefangen werden. Besonders bei serverseitigen Anwendungen, Hintergrundprozessen oder Microservices, die APIs regelmäßig abfragen müssen, ist es unverzichtbar.
Darüber hinaus lässt sich das Pattern gut mit sogenannten Backoff-Strategien kombinieren, wie z.B. mit einem exponential backoff, bei dem die Wartezeit zwischen den Versuchen exponentiell wächst. Diese Technik schützt APIs vor Überlastung und erhöht die Erfolgswahrscheinlichkeit bei andauernden Problemen.
async function retryWithExponentialBackoff(fn, retries = 5, delay = 500) {
try {
return await fn();
} catch (err) {
if (retries > 0) {
const wait = delay * Math.pow(2, 5 - retries);
console.warn(`Retry in ${wait}ms...`);
await new Promise(res => setTimeout(res, wait));
return retryWithExponentialBackoff(fn, retries - 1, delay);
} else {
throw err;
}
}
}
// Beispielverwendung
const fetchData = () => fetch('https://api.example.com/data');
retryWithExponentialBackoff(fetchData, 10, 500)
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.error("Fehlgeschlagen nach mehreren Versuchen:", err));
Nachteile und Fallstricke
Trotz aller Vorteile ist Vorsicht geboten. Ein blindes Wiederholen kann gefährlich sein. So zum Beispiel, wenn der Fehler nicht temporär ist, sondern strukturell (etwa ein 404-Fehler oder falsche Zugangsdaten). Hier macht ein Retry keinen Sinn und führt nur zu unnötiger Last und Verzögerung.
Zudem kann das Pattern bei falscher Anwendung zu Endlosschleifen oder schwer zu debuggenden Fehlern führen. Die Anzahl der Retries sollte also immer festgelegt werden.
Eine weitere Herausforderung liegt in der Kombination mit Transaktionen oder datenverändernden Operationen. Wird etwa ein POST-Request wiederholt, kann dies zu doppelten Datensätzen führen, wenn der erste Versuch auf Serverseite doch erfolgreich war, aber die Antwort nicht korrekt ankam.
Fazit
Das Retry Pattern ist ein sinnvolles Werkzeug, um JavaScript-Anwendungen fehlertoleranter und robuster zu gestalten. Besonders in asynchronen oder verteilten Systemen hilft es dabei, temporäre Probleme elegant abzufangen und ein besseres Nutzererlebnis zu bieten.
Gleichzeitig sollte das Pattern mit Bedacht eingesetzt werden. Nicht jede Fehlerquelle rechtfertigt einen Retry und zu viele Wiederholungen können negative Effekte haben. Wer das Retry Pattern jedoch gezielt und überlegt einsetzt – etwa in Kombination mit Backoff-Strategien und Fehleranalysen – profitiert von einer spürbar stabileren Codebasis.
FAQ: Häufige Fragen zum Retry Pattern in JavaScript
Wann sollte ich das Retry Pattern anwenden und wann nicht?
Das Retry Pattern eignet sich besonders gut bei temporären, unvorhersehbaren Fehlern, wie Netzwerkproblemen, Timeouts oder kurzfristig überlasteten Servern (HTTP 429 oder 503). In diesen Fällen ist die Wahrscheinlichkeit hoch, dass ein erneuter Versuch erfolgreich ist.
Dagegen sollte man auf Retries verzichten, wenn es sich um permanente oder logische Fehler handelt, z. B. Authentifizierungsprobleme (401), falsche Endpunkte (404) oder Validierungsfehler (422). Hier bringt ein Wiederholen nichts und könnte sogar Schaden anrichten.
async function safeRetry(fn, retries = 3) {
try {
const result = await fn();
return result;
} catch (error) {
if (error.response && [500, 503, 504].includes(error.response.status)) {
if (retries > 0) return safeRetry(fn, retries - 1);
}
throw error; // permanente Fehler sofort weitergeben
}
}
Wie viele Retry-Versuche sind sinnvoll?
Die ideale Anzahl hängt vom Anwendungskontext ab. Allgemein haben sich 3 bis 5 Versuche bewährt. Mehr als das kann zu langen Wartezeiten und unnötiger Belastung des Servers führen. Das gilt insbesondere wenn die Retry-Zeit exponentiell ansteigt.
Für APIs mit Rate Limiting (z. B. Twitter oder GitHub API) sollte man außerdem die Retry-After-Header respektieren, falls vorhanden.
// 3 Versuche, dann Abbruch
retry(() => fetch('/api/data'), 3, 1000);
Was ist ein Exponential Backoff und warum ist das wichtig?
Ein Exponential Backoff erhöht die Wartezeit zwischen den Versuchen exponentiell, z. B. 500ms, 1000ms, 2000ms usw. So entlastet man überlastete Systeme und verhindert, dass zu viele Anfragen gleichzeitig erneut gesendet werden. Häufig wird dieser Mechanismus durch Jitter ergänzt, also eine zufällige Zeitabweichung, um die Synchronisierung paralleler Anfragen zu vermeiden.
async function retryWithBackoff(fn, retries = 4, delay = 500) {
try {
return await fn();
} catch (err) {
if (retries === 0) throw err;
const backoff = delay * Math.pow(2, 4 - retries);
const jitter = Math.random() * 100;
await new Promise(res => setTimeout(res, backoff + jitter));
return retryWithBackoff(fn, retries - 1, delay);
}
}
Sollte ich jeden Fehler automatisch wiederholen?
Nein! Die automatische Wiederholung aller Fehler ist nicht ratsam. Man sollte unbedingt eine Fehlerauswertung einbauen und nur bei retry-fähigen Fehlern erneut versuchen. Dazu zählen:
- Netzwerkfehler (
ECONNRESET
,ETIMEDOUT
) - HTTP 500, 502, 503, 504
- Rate Limits (429) mit
Retry-After
-Header
Nicht retry-fähig sind:
- Authentifizierungsfehler (401)
- Ressourcen nicht gefunden (404)
- Payload zu groß (413)
if ([500, 502, 503, 504].includes(response.status)) {
return retry(fetchData);
}
Ist das Retry Pattern auch bei Datenbankzugriffen sinnvoll?
Ja, vor allem bei verbindungsbedingten Problemen wie Netzwerkausfällen oder kurzzeitiger Nichtverfügbarkeit der Datenbank. In produktionsreifen Anwendungen wird häufig bei Transient Errors (z. B. bei PostgreSQL oder MongoDB) automatisch ein Retry ausgelöst.
Wichtig: Bei schreibenden Operationen (z. B. INSERT
, UPDATE
) sollte man darauf achten, idempotente Aufrufe zu gewährleisten, um doppelte Einträge zu vermeiden.
const dbRetry = () => db.query('SELECT * FROM users WHERE active = true');
retry(dbRetry, 3, 500);
Wie kann ich verhindern, dass ein Retry unendlich lange läuft?
Es gibt zwei Strategien:
- Retry-Limit festlegen (z. B. max. 5 Versuche)
- Timeout oder Deadline verwenden, z. B. Abbruch nach 10 Sekunden Gesamtzeit
async function retryWithDeadline(fn, maxRetries = 5, timeout = 10000) {
const start = Date.now();
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (err) {
if (Date.now() - start > timeout) throw new Error("Retry-Timeout erreicht");
await new Promise(res => setTimeout(res, 500));
}
}
throw new Error("Retry fehlgeschlagen");
}
Funktioniert das Retry Pattern mit async/await
gut zusammen?
Ja, sogar hervorragend. Das async/await
-Pattern erlaubt eine saubere, lineare Struktur des Retry-Prozesses und erhöht die Lesbarkeit enorm.
async function fetchWithRetry() {
return await retry(() => fetch('/api/info'), 3, 1000);
}
Wie kann ich das Retry Pattern testen?
Man testet Retry-Logik am besten mit Mock-Funktionen, die gezielt Fehler werfen. Mit Jest oder Sinon lassen sich kontrollierte Fehlerszenarien simulieren. So kann man verifizieren, ob die Retry-Funktion korrekt wiederholt und am Ende wie gewünscht fehlschlägt oder Erfolg hat.
const mockFn = jest.fn()
.mockRejectedValueOnce(new Error('Fehler 1'))
.mockRejectedValueOnce(new Error('Fehler 2'))
.mockResolvedValue('OK');
await retry(mockFn, 3);
expect(mockFn).toHaveBeenCalledTimes(3);
Gibt es Libraries, die Retry-Funktionalität bieten?
Ja. Wer nicht selbst implementieren möchte, kann auf bewährte Libraries zurückgreifen, z. B.:
- p-retry – leichtgewichtig, konfigurierbar
- axios-retry – speziell für Axios-HTTP-Clients
import pRetry from 'p-retry';
const run = () => fetch('https://api.example.com/data');
await pRetry(run, { retries: 4 });
Ist das Retry Pattern auch im Frontend sinnvoll?
Ja, besonders bei mobilen Geräten, die mit instabilen Verbindungen kämpfen. Auch bei Push-Nachrichten, PWA-Synchronisation oder periodischen API-Pings im Hintergrund macht das Pattern Sinn. Man kann z. B. bei einem fetch()
automatisch ein Retry einbauen, um die User Experience zu verbessern.
retry(() => fetch('/api/status'), 2, 1000)
.then(r => r.json())
.then(console.log)
.catch(() => showOfflineMessage());
Buy me a coffee
Wenn Dir meine Beiträge gefallen und sie Dir bei Deiner Arbeit helfen, würde ich mich über einen “Kaffee” und ein paar nette Worte von Dir freuen.
Weiterführende externe Links:
axios-retry - Axios-Plugin, das fehlgeschlagene Anfragen abfängt und sie nach Möglichkeit wiederholt
p-retry - Wiederholen Sie eine Promise-Returning- oder asynchrone Funktion