Promise & Async
Retry logic, timeouts, concurrency limits, polling, and other async patterns that every JavaScript developer ends up needing and Googling.
retry
Re-run an async function up to N times before giving up.
const retry = async (fn, retries = 3, delay = 500) => {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === retries) throw err;
await new Promise((r) => setTimeout(r, delay * attempt)); // exponential back-off
}
}
};
// Usage
const data = await retry(() => fetch("/api/data").then((r) => r.json()), 3, 1000);
withTimeout
Reject a promise if it doesn't resolve within a given time.
const withTimeout = (promise, ms, message = "Operation timed out") =>
Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(message)), ms)
),
]);
// Usage
try {
const data = await withTimeout(fetch("/api/slow"), 5000);
} catch (err) {
console.error(err.message); // "Operation timed out"
}
pLimit — limit concurrent promises
Run at most N promises at a time (rate limiting without a library).
const pLimit = (concurrency) => {
let active = 0;
const queue = [];
const next = () => {
if (active >= concurrency || queue.length === 0) return;
active++;
const { fn, resolve, reject } = queue.shift();
fn().then(resolve, reject).finally(() => {
active--;
next();
});
};
return (fn) =>
new Promise((resolve, reject) => {
queue.push({ fn, resolve, reject });
next();
});
};
// Usage: fetch 100 URLs but only 5 at a time
const limit = pLimit(5);
const results = await Promise.all(
urls.map((url) => limit(() => fetch(url).then((r) => r.json())))
);
delay
Pause execution for N milliseconds.
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Usage
await delay(1000); // wait 1 second
console.log("1 second later");
// With async/await in a loop
for (const item of items) {
await processItem(item);
await delay(200); // rate-limit API calls
}
allSettled with results
Promise.allSettled — get both fulfilled and rejected results without throwing.
const results = await Promise.allSettled([
fetch("/api/users"),
fetch("/api/posts"),
fetch("/api/missing-endpoint"),
]);
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.error("Failed:", result.reason);
}
});
tryCatch — inline error handling
Avoid nested try/catch blocks with a wrapper that returns [error, data].
const tryCatch = async (promise) => {
try {
const data = await promise;
return [null, data];
} catch (err) {
return [err, null];
}
};
// Usage — Go-style error handling
const [err, user] = await tryCatch(fetchUser(id));
if (err) {
console.error("Failed to fetch user:", err.message);
return;
}
console.log(user.name);
poll
Repeatedly call an async function until a condition is met or it times out.
const poll = async (fn, { interval = 1000, timeout = 30000, condition = (r) => !!r } = {}) => {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const result = await fn();
if (condition(result)) return result;
await new Promise((r) => setTimeout(r, interval));
}
throw new Error("Polling timed out");
};
// Usage: wait for a job to finish
const job = await poll(
() => fetch(`/api/jobs/${id}`).then((r) => r.json()),
{
interval: 2000,
timeout: 60000,
condition: (job) => job.status === "done",
}
);
runSeries
Run async functions one after another and collect all results.
const runSeries = async (tasks) => {
const results = [];
for (const task of tasks) {
results.push(await task());
}
return results;
};
// Usage
const results = await runSeries([
() => fetchUser(1),
() => fetchPosts(1),
() => fetchComments(1),
]);
memoizeAsync
Cache the result of an async function (prevents duplicate in-flight requests).
const memoizeAsync = (fn) => {
const cache = new Map();
return async (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const promise = fn(...args);
cache.set(key, promise);
try {
return await promise;
} catch (err) {
cache.delete(key); // don't cache failures
throw err;
}
};
};
// Usage: multiple calls with same args share one request
const getUser = memoizeAsync((id) => fetch(`/api/users/${id}`).then((r) => r.json()));
// These three fire only ONE network request
const [a, b, c] = await Promise.all([getUser(1), getUser(1), getUser(1)]);
Summary
| Utility | Purpose |
|---|---|
retry(fn, n, delay) | Re-run on failure with back-off |
withTimeout(promise, ms) | Reject if too slow |
pLimit(n) | Max N concurrent promises |
delay(ms) | Async sleep |
Promise.allSettled | Run all, collect both successes and failures |
tryCatch(promise) | Returns [err, data] — no try/catch blocks |
poll(fn, opts) | Repeat until condition is met |
runSeries(tasks) | Sequential async execution |
memoizeAsync(fn) | Cache + deduplicate async calls |