Yieldless
Reference

yieldless/all

Tuple-aware parallel combinators that share cancellation.

yieldless/all gives you helpers for tuple-returning parallel work: all(tasks) waits for every task or aborts siblings on the first error, race(tasks) resolves with the first settled result and aborts the rest, and mapLimit(items, mapper, options) processes a collection with bounded concurrency.

Exports

  • type SafeTask<T, E = Error> = (signal: AbortSignal) => PromiseLike<SafeResult<T, E>> | SafeResult<T, E>
  • type MapLimitMapper<Item, Value, E = Error> = (item: Item, index: number, signal: AbortSignal) => PromiseLike<SafeResult<Value, E>> | SafeResult<Value, E>
  • all(tasks, options): Promise<SafeResult<AllValues<Tasks>, ParallelError<Tasks>>>
  • mapLimit(items, mapper, { concurrency, signal }): Promise<SafeResult<Value[], E>>
  • race(tasks, options): Promise<SafeResult<Value, Error>>

Example

import { all } from "yieldless/all";
import { safeTry } from "yieldless/error";

const result = await all([
  (signal) => safeTry(readPrimary(signal)),
  (signal) => safeTry(readReplica(signal)),
]);

For large batches, use mapLimit() to avoid starting every item at once:

import { mapLimit } from "yieldless/all";
import { safeTry } from "yieldless/error";

const [error, avatars] = await mapLimit(
  users,
  (user, _index, signal) =>
    safeTry(fetchAvatar(user.avatarUrl, { signal })),
  { concurrency: 4 },
);

Behavior notes

  • all([]) succeeds with an empty array.
  • mapLimit([], mapper, options) succeeds with an empty array.
  • mapLimit() preserves input order and throws a RangeError when concurrency is less than 1 or not an integer.
  • race([]) throws a RangeError.
  • If any task or mapped item returns [error, null], siblings are aborted before the final tuple is returned.
  • race() aborts losing tasks immediately, then waits for them to settle before it returns.
  • Thrown task and mapper failures are normalized into tuple failures internally.

When to prefer runTaskGroup() instead

Use all(), race(), and mapLimit() when the work is already tuple-native. Use runTaskGroup() when you want imperative fan-out and regular promise values.

Good

Use all() for a small, fixed set of independent tuple tasks.

const [error, [profile, permissions]] = await all([
  (signal) => loadProfile(userId, signal),
  (signal) => loadPermissions(userId, signal),
]);

Use mapLimit() when a list could be large or expensive.

const [error, summaries] = await mapLimit(
  repositories,
  (repo, _index, signal) => readSummary(repo.path, signal),
  { concurrency: 4, signal },
);

Use race() when the first success or first failure should settle the operation.

const result = await race([
  (signal) => readPrimary(signal),
  (signal) => readReplica(signal),
]);

Avoid

Do not pass work that ignores the signal and expect cancellation to be immediate.

await all([
  async () => safeTry(expensiveCpuLoop()),
  async () => safeTry(readRemoteData()),
]);

Do not use all() for thousands of items. Use mapLimit() or yieldless/iterable so you can control pressure.

On this page