Skip to content

Async Patterns

Recipes built from sleep, timeout, retry, defer, and parallelLimit.

Cancellable fetch with timeout

ts
import { timeout, sleep } from '@pivanov/utils/promise';

async function cancellableFetch(url: string, ms: number, signal?: AbortSignal) {
  const ctrl = new AbortController();
  signal?.addEventListener('abort', () => ctrl.abort(signal.reason));

  return timeout(fetch(url, { signal: ctrl.signal }), ms);
}

Retry with exponential backoff and jitter

ts
import { retry } from '@pivanov/utils/promise';

const jitter = (ms: number) => ms * (0.75 + Math.random() * 0.5);

const data = await retry(() => fetch('/api').then((r) => r.json()), {
  attempts: 5,
  backoff: (n) => jitter(100 * 2 ** n),
});

Retry only on specific errors

ts
import { retry } from '@pivanov/utils/promise';

await retry(
  () => callApi(),
  {
    attempts: 3,
    shouldRetry: (err) => {
      // Don't retry on 4xx - only network / 5xx
      if (err instanceof HttpError && err.status < 500) {
        return false;
      }
      return true;
    },
  },
);

Defer to coordinate handlers

ts
import { defer } from '@pivanov/utils/promise';

const waitForConnect = () => {
  const { promise, resolve, reject } = defer<void>();
  socket.once('connect', () => resolve());
  socket.once('error', (err) => reject(err));
  return promise;
};

await waitForConnect();

Bulk fetch with bounded concurrency

ts
import { parallelLimit } from '@pivanov/utils/promise';

const urls = [...];
const responses = await parallelLimit(urls, 6, async (url) => {
  const res = await fetch(url);
  return res.json();
});

TIP

parallelLimit preserves input order - responses[i] corresponds to urls[i].

Polling until a condition

ts
import { sleep } from '@pivanov/utils/promise';

async function pollUntil<T>(
  check: () => Promise<T | null>,
  intervalMs: number,
  signal?: AbortSignal,
): Promise<T> {
  while (true) {
    signal?.throwIfAborted();
    const result = await check();
    if (result !== null) {
      return result;
    }
    await sleep(intervalMs, signal);
  }
}

Cancellable sleep

ts
import { sleep } from '@pivanov/utils/promise';

const ctrl = new AbortController();

setTimeout(() => ctrl.abort(new Error('user navigated away')), 200);

try {
  await sleep(1000, ctrl.signal);
} catch (err) {
  // err is the abort reason
}

Timing out any user-land promise

ts
import { timeout } from '@pivanov/utils/promise';

try {
  await timeout(doExpensiveThing(), 5000);
} catch (err) {
  if (err instanceof Error && err.message.includes('timed out')) {
    // handle slow path
  }
}

MIT License | Made by Pavel Ivanov