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.
Promiseandasync/awaitfor sequencingAbortControllerandAbortSignalfor cancellationAsyncLocalStoragefor async context in NodeSymbol.asyncDisposeandawait usingfor 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
| Problem | Start with |
|---|---|
| You want cleaner async errors | yieldless/error |
| You want to transform tuple values | yieldless/result |
| You need sibling cancellation | yieldless/task |
| You need bounded batch work | yieldless/all or yieldless/iterable |
| You need abort-aware retries | yieldless/retry |
| You need a firm deadline | yieldless/signal |
| You need polling or sleep | yieldless/timer |
| You need HTTP JSON with deadlines | yieldless/fetch |
| You need startup config | yieldless/env |
| You want typed route handlers | yieldless/router |
| You are building an Electron boundary | yieldless/ipc |
| You need in-flight deduplication | yieldless/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 withyieldless/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.