npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

async-toolkit

v0.5.0

Published

Zero-dependency async toolkit for TypeScript — retry, timeout, pLimit, pMap, sleep, defer & error-as-value. Tree-shakeable, ESM + CJS.

Readme

async-toolkit

The async primitives you reach for in every project — in one zero-dependency, tree-shakeable package that works in both ESM and CommonJS.

Instead of installing and version-juggling p-retry + p-limit + p-timeout + p-map + await-to-js separately, get them all from one cohesive, fully-typed toolkit.

  • 🪶 Zero dependencies, fully tree-shakeable — unused helpers add nothing to your bundle
  • 🔀 ESM and CommonJS — unlike the p-* family, which is ESM-only
  • 🔒 First-class TypeScript — generics, narrowing, and a shared AbortSignal convention across every helper that waits or does work (sleep, retry, timeout, pMap)
  • ✅ Works in Node ≥ 18 and modern browsers

Why not just use p-retry, p-limit, …?

Those are excellent packages (and the inspiration here), but they have two friction points this toolkit removes:

| | p-* family | async-toolkit | | ----------------- | --------------------------- | -------------------------------------------- | | Install footprint | one package per helper | one package | | Module formats | ESM only (no require) | ESM + CJS | | API consistency | each its own conventions | one consistent options & AbortSignal style | | Bundle cost | per-package overhead | shared internals, tree-shaken |

If you're already all-ESM and only need one helper, the single-purpose packages are great. If you want one dependency, CJS support, and a consistent API, reach for this.

Install

npm install async-toolkit

Helpers

to — handle errors without try/catch

import { to } from "async-toolkit";

const [err, user] = await to(fetchUser(id));
if (err) {
  // `err` is `unknown` — narrow it before use
  const message = err instanceof Error ? err.message : String(err);
  return res.status(500).send(message);
}
console.log(user.name); // `user` narrowed to non-null

err is typed as unknown (anything can be thrown in JS), so narrow it with instanceof before use. Pass an explicit type if you know the shape: to<User, ApiError>(fetchUser(id)).

You can also pass a function instead of a promise — handy for wrapping synchronous code, since to then captures a thrown error too:

const [err, config] = await to(() => JSON.parse(raw));

retry — exponential backoff

import { retry } from "async-toolkit";

const data = await retry((attempt, signal) => fetchFlaky({ signal }), {
  attempts: 5, // total tries incl. the first (default 3)
  delay: 200, // base delay ms (default 100)
  factor: 2, // backoff multiplier (default 2)
  maxDelay: 5000, // cap per-wait delay
  jitter: true, // randomize delays to avoid thundering herds
  shouldRetry: async (err) => err instanceof NetworkError, // sync or async
  onRetry: (err, attempt) => console.warn(`retry #${attempt}`, err),
});

fn receives the attempt number and the signal. Aborting the signal rejects with AbortError and interrupts the in-flight attempt immediately — even if fn ignores the signal — as well as cancelling any pending backoff wait.

timeout — bound latency

import { timeout, TimeoutError } from "async-toolkit";

try {
  const data = await timeout(fetch(url), 5000);
} catch (err) {
  if (err instanceof TimeoutError) console.log(`timed out after ${err.ms}ms`);
}

Pass an AbortSignal as the third argument to reject the wait early with AbortError.

To actually cancel the underlying work on timeout (not just stop waiting), pass a (signal) => Promise factory instead of a promise. timeout aborts that signal when the deadline passes or the external signal fires, so the work frees its resources:

// the fetch is aborted when the 5s deadline passes
const data = await timeout((signal) => fetch(url, { signal }), 5000);

// also cancellable from outside
const ac = new AbortController();
const data = await timeout((signal) => fetch(url, { signal }), 5000, ac.signal);

pLimit — cap concurrency

import { pLimit } from "async-toolkit";

const limit = pLimit(2); // at most 2 in flight at once
const results = await Promise.all(urls.map((url) => limit(() => fetch(url))));

limit.activeCount; // currently running
limit.pendingCount; // waiting in the queue

limit.concurrency = 5; // raise/lower the limit — raising starts queued tasks now
limit.clearQueue(); // drop not-yet-started tasks (their promises stay pending)
limit.clearQueue(reason); // ...or reject those pending promises with `reason`

pMap — concurrency-limited map

import { pMap } from "async-toolkit";

const bodies = await pMap(
  urls,
  // each mapper gets a signal that aborts if the map is cancelled
  (url, _i, signal) => fetch(url, { signal }).then((r) => r.text()),
  {
    concurrency: 4,
    stopOnError: false, // aggregate failures into an AggregateError
    signal: ac.signal, // abort early, discarding queued mappers
  },
);

Results are returned in input order regardless of which mapper settles first. With stopOnError: false, the aggregated AggregateError.errors are likewise ordered by input position, not by when each mapper failed. The mapper's third argument is an AbortSignal that fires when the map is cancelled — via signal, or (with stopOnError) when a sibling mapper fails — so in-flight mappers can stop their own work instead of running on in vain.

sleep — cancellable delay

import { sleep } from "async-toolkit";

await sleep(1000);

const ac = new AbortController();
sleep(5000, ac.signal).catch(() => console.log("cancelled"));
ac.abort();

defer — externally-settled promise

import { defer } from "async-toolkit";

const d = defer<string>();
emitter.once("ready", () => d.resolve("ok"));
emitter.once("error", (e) => d.reject(e));
const result = await d.promise;

API

| Export | Description | | ------------------------------- | ----------------------------------------------------------------------------------------- | | to(promise \| fn) | Resolves to [error, null] or [null, value]. | | retry(fn, options?) | Retries fn with exponential backoff. | | timeout(promise, ms, signal?) | Rejects with TimeoutError if promise is too slow, or AbortError if signal aborts. | | pLimit(concurrency) | Returns a function that limits concurrent tasks. | | pMap(items, mapper, options?) | Concurrency-limited, order-preserving async map. | | sleep(ms, signal?) | Cancellable delay. | | defer() | A promise plus its resolve/reject. |

Types Result, RetryOptions, LimitFunction, PMapOptions, Deferred and errors TimeoutError, AbortError are also exported.

Development

npm install
npm test          # run the test suite
npm run build     # bundle to dist/
npm run typecheck # tsc --noEmit

License

MIT