Yieldless
Guides

Design Rules

The principles behind Yieldless and the tradeoffs those principles deliberately make.

Yieldless is small on purpose. The design rules are what keep it small.

1. Prefer native language features over framework runtimes

If JavaScript or Node already has a solid primitive for the job, Yieldless uses it.

  • Promise and async/await for sequencing
  • AbortController and AbortSignal for cancellation
  • AsyncLocalStorage for async context in Node
  • Symbol.asyncDispose and await using for resource cleanup

This rule keeps the library easy to explain to engineers who did not build the original system.

2. Keep failures visible

Thrown exceptions are still useful for process-level failures and framework boundaries, but routine operational failures should stay explicit.

const [error, value] = await safeTry(readConfig());

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

That shape is intentionally repetitive. It makes failure handling obvious during code review.

3. Cancellation is cooperative

runTaskGroup(), all(), race(), and safeRetry() all respect AbortSignal, but they cannot cancel code that ignores the signal.

group.spawn((signal) => runCommand("git", ["fetch"], { signal }));

4. Keep adapters thin

The schema, router, IPC, and Node modules are adapters. They are not attempts to replace the libraries they sit beside.

5. Avoid global magic

Use inject() for stable dependencies. Use createContext() for request-scoped data.

6. Pick the smallest module that solves the problem

ProblemStart with
You want cleaner async errorsyieldless/error
You want to transform tuple valuesyieldless/result
You need sibling cancellationyieldless/task
You need bounded batch workyieldless/all or yieldless/iterable
You need abort-aware retriesyieldless/retry
You need a firm deadlineyieldless/signal
You need polling or sleepyieldless/timer
You need HTTP JSON with deadlinesyieldless/fetch
You need startup configyieldless/env
You want typed route handlersyieldless/router
You are building an Electron boundaryyieldless/ipc
You need in-flight deduplicationyieldless/singleflight

7. Re-throw only at the boundary that needs it

unwrap() exists for places that genuinely expect thrown exceptions.

import { safeTry, unwrap } from "yieldless/error";

await transaction(async () => {
  const result = await safeTry(writeModel());
  return unwrap(result);
});

8. Prefer adapters over replacements

Yieldless should sit beside the tools teams already like.

  • Use your schema library, adapt it with yieldless/schema.
  • Use platform fetch, wrap it with yieldless/fetch.
  • Use Hono-style handlers, adapt tuple results with yieldless/router.
  • Use Electron IPC, preserve tuple results with yieldless/ipc.

9. Keep pressure explicit

When work can fan out, make the pressure control visible in code.

await mapLimit(repositories, refreshRepository, {
  concurrency: 4,
  signal,
});

That one option is often the difference between a helpful tool and a machine that feels frozen.

On this page