Recipes
Resilient Service Flow
A practical backend flow that combines tuples, retries, validation, routing, and cancellation.
This recipe shows the shape Yieldless is best at: an HTTP request that validates input, performs a few pieces of I/O, retries the flaky part, and returns a normal JSON response.
The moving parts
yieldless/schemavalidates input without throwingyieldless/fetchkeeps HTTP calls in tuple formyieldless/retryhandles transient I/O noiseyieldless/taskkeeps sibling work under one cancellation signalyieldless/routerturns tuple results into a plain response
Route handler
import { safeTry } from "yieldless/error";
import { parseSafe } from "yieldless/schema";
import { safeRetry } from "yieldless/retry";
import { NotFoundError, honoHandler } from "yieldless/router";
import { runTaskGroup } from "yieldless/task";
export const getRepository = honoHandler(async (c) => {
const [paramsError, params] = parseSafe(repositoryParamsSchema, c.req.param());
if (paramsError) {
return [paramsError, null];
}
const [repoError, repo] = await safeRetry(
async (_attempt, signal) => safeTry(loadRepository(params.id, signal)),
{ maxAttempts: 3, baseDelayMs: 100 },
);
if (repoError) {
return [repoError, null];
}
if (repo === null) {
return [new NotFoundError("Repository not found"), null];
}
const payload = await runTaskGroup(async (group) => {
const refs = group.spawn((signal) => loadRefs(repo.path, signal));
const status = group.spawn((signal) => loadStatus(repo.path, signal));
return {
id: repo.id,
refs: await refs,
status: await status,
};
});
return [null, payload];
});Why this holds up well in production
- Validation failures never take the exception path.
- If
loadRefs()fails,loadStatus()is aborted immediately. - Retry timers are cancellable because they run through
AbortSignal. - The handler body stays linear. There is no framework-specific DSL to learn.
Good variation: add a remote dependency
import { fetchJsonSafe, HttpStatusError } from "yieldless/fetch";
const [metadataError, metadata] = await safeRetry(
(_attempt, signal) =>
fetchJsonSafe<RepositoryMetadata>(metadataUrl(repo.id), {
timeoutMs: 3_000,
signal,
}),
{
maxAttempts: 3,
shouldRetry: (error) =>
!(error instanceof HttpStatusError) || error.status >= 500,
},
);
if (metadataError) {
return [metadataError, null];
}The retry policy is attached to the flaky network call, not to the whole request.
Avoid: retrying the whole handler
export const getRepository = honoHandler((c) =>
safeRetry(() => loadWholeRepositoryResponse(c), {
maxAttempts: 3,
}),
);That repeats validation, routing decisions, and any side effects. Retry the noisy boundary instead.
Rules worth keeping
- Validate early and return early.
- Retry only the noisy boundary, not the entire request.
- Spawn sibling work only when both tasks should die together.
- Map domain misses to explicit HTTP errors like
NotFoundError.