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

nurserykit

v0.1.0

Published

Structured concurrency for async/await: withNursery (nursery/TaskGroup pattern), race with AbortSignal cancellation, sleep, withTimeout, anySignal, retry. Zero dependencies. Like Python asyncio.TaskGroup / Go errgroup, no generators required.

Readme

nurserykit

All Contributors

Structured concurrency for standard async/await — no generators required.

withNursery (nursery/TaskGroup pattern) · race with automatic AbortSignal cancellation · sleep · withTimeout · anySignal · retry

npm npm downloads License: MIT

Why nurserykit?

JavaScript has Promise.all and Promise.race, but nothing that gives you:

  1. Structured lifetime — child tasks cannot outlive the scope that created them
  2. Automatic cancellation — if any task throws, the others are cancelled via AbortSignal
  3. Error aggregation — collects all errors into TaskGroupError (like Python's ExceptionGroup)

Effection is the closest npm equivalent but requires rewriting all your async functions as generators. nurserykit works with standard async/await and the native AbortSignal API — drop it into any Node.js 18+ codebase with zero changes to existing code.

Install

npm install nurserykit

Quick start

import { withNursery, race, sleep } from "nurserykit";

// Fetch two resources concurrently — if one throws, the other is cancelled
await withNursery(async (nursery) => {
  nursery.spawn(async (signal) => {
    const user = await fetch("/api/user", { signal }).then(r => r.json());
    console.log(user);
  });
  nursery.spawn(async (signal) => {
    const posts = await fetch("/api/posts", { signal }).then(r => r.json());
    console.log(posts);
  });
});

// Race two endpoints — winner resolved, loser cancelled
const { value, index } = await race([
  (signal) => fetch("https://api1.example.com/data", { signal }).then(r => r.json()),
  (signal) => fetch("https://api2.example.com/data", { signal }).then(r => r.json()),
]);

// Cancellable sleep
await sleep(1000, signal);

API

withNursery(fn, opts?)

Run fn with a structured concurrency scope. All tasks spawned inside fn are awaited before withNursery returns.

async function withNursery<T>(
  fn: (nursery: Nursery) => Promise<T>,
  opts?: {
    signal?: AbortSignal;  // cancel scope from outside
    timeout?: number;      // cancel after N milliseconds
  },
): Promise<T>

Guarantees:

  • All spawned tasks complete (or are cancelled) before the scope exits
  • If any task throws → scope AbortSignal is aborted → remaining tasks receive the signal → all errors collected → rethrown as TaskGroupError
  • If fn itself throws → all running tasks are cancelled
  • If signal fires → all tasks are cancelled → CancelledError is thrown
// Fail-fast: one task fails, other is cancelled
try {
  await withNursery(async (nursery) => {
    nursery.spawn(async (signal) => {
      await fetchData(signal); // this throws
    });
    nursery.spawn(async (signal) => {
      await slowBackup(signal); // this gets AbortSignal abort
    });
  });
} catch (err) {
  // err is the original error from fetchData
}

// Timeout: cancel everything after 5 seconds
await withNursery(
  async (nursery) => {
    nursery.spawn(async (signal) => longOperation(signal));
  },
  { timeout: 5000 },
);

// External cancellation
const controller = new AbortController();
button.onclick = () => controller.abort();

await withNursery(
  async (nursery) => {
    nursery.spawn(async (signal) => streamResults(signal));
  },
  { signal: controller.signal },
);

nursery.spawn(task)

Add a task to the scope. Returns a Promise with the task's result.

nursery.spawn<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T>

Tasks receive the scope's AbortSignal automatically — pass it to fetch, sleep, and other cancellable APIs.

nursery.signal

The scope's AbortSignal. Aborted when any task fails or the scope is cancelled.


race(tasks, parentSignal?)

Run tasks concurrently and return the first successful result. Losers are cancelled via AbortSignal.

async function race<T>(
  tasks: ReadonlyArray<(signal: AbortSignal) => Promise<T>>,
  parentSignal?: AbortSignal,
): Promise<{ value: T; index: number }>
  • Returns { value, index } from the winning task
  • Skips failed tasks and waits for the next success
  • Throws TaskGroupError only if all tasks fail
  • Throws CancelledError if parentSignal is aborted
// Redundant fetch — use whichever responds first
const { value: data } = await race([
  (signal) => fetch("https://us.api.example.com", { signal }).then(r => r.json()),
  (signal) => fetch("https://eu.api.example.com", { signal }).then(r => r.json()),
]);

// With parent cancellation
const { value } = await race([task1, task2], controller.signal);

raceSettled(tasks, parentSignal?)

Like race but returns the first task to settle (resolve OR reject), regardless of success.

const result = await raceSettled([task1, task2]);
// result: { status: "fulfilled", value: T, index: number }
//       | { status: "rejected", reason: unknown, index: number }

sleep(ms, signal?)

Pause for ms milliseconds. Rejects with CancelledError if signal fires.

await sleep(1000);
await sleep(1000, signal); // cancellable

withTimeout(promise, ms, signal?)

Wrap any Promise with a deadline. Throws TimeoutError if not settled in time.

const data = await withTimeout(fetch(url), 5000);
const data = await withTimeout(fetch(url, { signal }), 5000, signal);

timeoutSignal(ms)

Create an AbortSignal that fires after ms milliseconds.

const signal = timeoutSignal(5000);
await fetch(url, { signal }); // auto-cancelled after 5s

anySignal(signals)

Merge multiple AbortSignals into one that fires when any input fires. Polyfills AbortSignal.any() for Node 18.

const signal = anySignal([userSignal, timeoutSignal(5000)]);
await fetch(url, { signal });

retry(task, opts?)

Retry a task up to retries times with optional delay between attempts.

const data = await retry(
  async (attempt, signal) => {
    console.log(`Attempt ${attempt}`);
    return fetch(url, { signal }).then(r => r.json());
  },
  { retries: 3, delay: 500, signal },
);

Error types

| Class | Extends | When thrown | |-------|---------|-------------| | CancelledError | Error | Scope or task cancelled via AbortSignal | | TimeoutError | Error | withTimeout or timeout option expired | | TaskGroupError | AggregateError | Multiple tasks failed simultaneously; .errors has all causes |

import { isCancelled } from "nurserykit";

try {
  await withNursery(/* ... */);
} catch (err) {
  if (isCancelled(err)) return; // ignore user cancellation
  throw err;
}

Comparison

| Feature | nurserykit | Effection | Promise.all | |---------|-----------|-----------|------------| | Standard async/await | ✅ | ❌ (generators) | ✅ | | Auto-cancel on error | ✅ | ✅ | ❌ | | AbortSignal threading | ✅ | ✅ | ❌ | | Error aggregation | ✅ | ✅ | partial | | race() with cancellation | ✅ | ❌ | ❌ | | Zero dependencies | ✅ | ❌ | — | | Bundle size | ~2KB | ~50KB | — |

Contributors ✨

This project follows the all-contributors specification. Contributions of any kind are welcome — code, docs, bug reports, ideas, reviews! See the emoji key for how each contribution is recognized, and open a PR or issue to get involved.

Thanks goes to these wonderful people:

License

MIT