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

UtilityPurpose
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.allSettledRun 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