Handling async operations in JavaScript can be a bit tricky, especially when things go sideways. Building a system to retry these operations can really beef up your app's reliability. Let’s walk through a practical way to do this.
Start by making a retry function that handles retrying stuff when it goes wrong. This func will take in an async operation, how many retries we want, and maybe a delay between them retries.
const retry = async (fn, retries = 3, delay = 1000) => {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === retries) {
throw new Error(`Operation failed after ${retries} attempts: ${error.message}`);
}
console.log(`Attempt ${attempt} failed. Retrying in ${delay} ms...`);
await new Promise(res => setTimeout(res, delay));
}
}
};
Now, let’s define the async task we want to retry. This could be anything that returns a promise, like fetching data from a server or querying a database.
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return await response.json();
};
Use the retry
function to execute your async operation. If it flops, it retries based on our earlier setup.
(async () => {
try {
const data = await retry(fetchData, 5, 2000); // 5 retries, 2000 ms delay
console.log('Data fetched successfully:', data);
} catch (error) {
console.error('Failed to fetch data:', error);
}
})();
One way to upgrade this method is by using exponential backoff for delays. This means the wait time grows with each retry. It can help lessen repetitive failures.
const retryWithExponentialBackoff = async (fn, retries = 3, baseDelay = 1000) => {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === retries) {
throw new Error(`Operation failed after ${retries} attempts: ${error.message}`);
}
const delay = baseDelay * Math.pow(2, attempt - 1);
console.log(`Attempt ${attempt} failed. Retrying in ${delay} ms...`);
await new Promise(res => setTimeout(res, delay));
}
}
};
// Use it
(async () => {
try {
const data = await retryWithExponentialBackoff(fetchData, 5, 1000); // 5 retries, starting with 1000 ms delay
console.log('Data fetched successfully:', data);
} catch (error) {
console.error('Failed to fetch data:', error);
}
})();
Another nice touch is to use an AbortController
for canceling the operation if needed.
const fetchDataWithAbort = async (abortSignal) => {
const response = await fetch('https://api.example.com/data', { signal: abortSignal });
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return await response.json();
};
const abortableRetry = async (fn, retries = 3, delay = 1000) => {
const controller = new AbortController();
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn(controller.signal);
} catch (error) {
if (error.name === 'AbortError') {
console.error('Operation was aborted');
return;
}
if (attempt === retries) {
throw new Error(`Operation failed after ${retries} attempts: ${error.message}`);
}
console.log(`Attempt ${attempt} failed. Retrying in ${delay} ms...`);
await new Promise(res => setTimeout(res, delay));
}
}
};
// Try aborting after a set time
setTimeout(() => {
controller.abort();
}, 5000); // Abort after 5 seconds
// Use abortable retry
(async () => {
try {
const data = await abortableRetry(fetchDataWithAbort, 5, 2000); // 5 retries, 2000 ms delay
console.log('Data fetched successfully:', data);
} catch (error) {
console.error('Failed to fetch data:', error);
}
})();
Logging each attempt and failure is super useful for monitoring. It helps identify where things go wrong and how often retries occur.
const retryWithLogging = async (fn, retries = 3, delay = 1000) => {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (error) {
console.error(`Attempt ${attempt} failed: ${error.message}`);
if (attempt === retries) {
throw new Error(`Operation failed after ${retries} attempts: ${error.message}`);
}
console.log(`Retrying in ${delay} ms...`);
await new Promise(res => setTimeout(res, delay));
}
}
};
// Use it
(async () => {
try {
const data = await retryWithLogging(fetchData, 5, 2000); // 5 retries, 2000 ms delay
console.log('Data fetched successfully:', data);
} catch (error) {
console.error('Failed to fetch data:', error);
}
})();