Yieldless
Guides

Module Selection

Choose the smallest Yieldless module for each kind of application work.

Yieldless is easiest to adopt when you choose one small module at the boundary where the pain appears. You do not need to move an application into a framework runtime. Keep ordinary TypeScript, add tuple and cancellation helpers where they remove noise, and stop there.

The capability map

Work you are doingStart withWhy
Convert a promise or sync throw into a tupleyieldless/errorEstablishes the [error, value] shape.
Transform or chain tuplesyieldless/resultRemoves repetitive branch plumbing without a DSL.
Run related promise work togetheryieldless/taskShared AbortSignal and sibling failure cleanup.
Run tuple tasks in parallelyieldless/allTuple-native all(), race(), and fixed-list mapLimit().
Process sync or async streams of valuesyieldless/iterablecollect(), sequential forEach(), and bounded iterable mapping.
Connect producers and consumersyieldless/queueBounded async queues with abortable offer/take backpressure.
Broadcast in-process eventsyieldless/pubsubAsync-iterable subscriptions with optional replay.
Protect shared capacity or API quotayieldless/limiterSemaphores and rate limiters for explicit backpressure.
Cache expensive async readsyieldless/cacheTTL/LRU caching with shared in-flight loads.
Batch nearby keyed readsyieldless/batcherDataLoader-style coalescing without another dependency.
Guard flaky dependenciesyieldless/breakerCircuit breaking for repeated tuple failures.
Retry transient tuple failuresyieldless/retryExponential backoff with jitter and abort-aware waits.
Reuse retry or polling timing rulesyieldless/scheduleComposable delay and stop policies without a scheduler.
Apply a time budgetyieldless/signalDisposable derived deadline signals.
Sleep or pollyieldless/timerAbort-aware timer utilities without a scheduler.
Fetch JSON or inspect HTTP statusyieldless/fetchNative fetch with tuple errors, timeouts, and JSON parsing.
Read process configurationyieldless/envRequired/optional env helpers and schema-backed parsing.
Validate unknown inputyieldless/schemaAdapter for existing schema libraries.
Build HTTP JSON handlersyieldless/routerConvert tuple handlers into responses.
Cross an Electron IPC boundaryyieldless/ipcTuple serialization and optional renderer-driven cancellation.
Use Node files or subprocessesyieldless/nodeTuple filesystem helpers and subprocess output capture.
Wait for one eventyieldless/eventAbortable EventTarget / EventEmitter waits.
Deduplicate in-flight workyieldless/singleflightPrevent duplicate calls from stampeding the same operation.
Scope cleanupyieldless/resourceNative await using for acquire/release pairs.
Bind stable dependenciesyieldless/diReader-like dependency binding with plain functions.
Carry request metadatayieldless/contextAsyncLocalStorage for request-scoped values and spans.
Test async helpersyieldless/testDeferred promises, manual clocks, and controlled abort signals.

Good adoption path

Start at the boundary where failures are already expected.

import { fetchJsonSafe } from "yieldless/fetch";

const [error, user] = await fetchJsonSafe<User>(url, {
  timeoutMs: 5_000,
  signal,
});

if (error) {
  return [error, null] as const;
}

return [null, user] as const;

Then add composition only when the code asks for it.

import { fromNullable, mapOk } from "yieldless/result";

return mapOk(
  fromNullable(user, () => new Error("User not found")),
  (value) => ({ id: value.id, name: value.name }),
);

Avoid turning Yieldless into a runtime

This is possible, but it is not the point:

// Avoid: a home-grown mini runtime with hidden policy everywhere.
const program = pipe(
  readConfig(),
  retryEverywhere(),
  injectGlobals(),
  runWithHiddenContext(),
);

Prefer direct code with explicit control flow.

const [configError, config] = parseEnvSafe(envSchema);
if (configError) return [configError, null] as const;

const [userError, user] = await safeRetry(
  (_attempt, signal) => fetchUser(config.apiUrl, signal),
  { maxAttempts: 3, signal },
);

Choosing between similar modules

Use yieldless/all when you already have a finite list of tuple tasks.

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

Use yieldless/task when the fan-out is imperative or the children return ordinary promise values.

await runTaskGroup(async (group) => {
  const refs = group.spawn((signal) => loadRefs(signal));
  const status = group.spawn((signal) => loadStatus(signal));

  return {
    refs: await refs,
    status: await status,
  };
});

Use yieldless/iterable when the input is a stream or async generator.

await mapAsyncLimit(readRows(filePath), processRow, {
  concurrency: 4,
  signal,
});

Use yieldless/singleflight when duplicate callers ask for the same work at the same time. It is not a cache.

const loadRepo = singleFlight(
  (signal, repoId: string) => readRepository(repoId, signal),
);

A complete boundary

import { fetchJsonSafe } from "yieldless/fetch";
import { safeRetry } from "yieldless/retry";
import { parseSafe } from "yieldless/schema";

export async function loadUserView(input: unknown, signal: AbortSignal) {
  const [inputError, params] = parseSafe(userParamsSchema, input);
  if (inputError) return [inputError, null] as const;

  const [userError, user] = await safeRetry(
    (_attempt, attemptSignal) =>
      fetchJsonSafe<User>(`/api/users/${params.id}`, {
        timeoutMs: 5_000,
        signal: attemptSignal,
      }),
    {
      maxAttempts: 3,
      signal,
    },
  );

  if (userError) return [userError, null] as const;

  return [
    null,
    {
      id: user.id,
      label: user.name,
    },
  ] as const;
}

On this page