Yieldless
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/schema validates input without throwing
  • yieldless/fetch keeps HTTP calls in tuple form
  • yieldless/retry handles transient I/O noise
  • yieldless/task keeps sibling work under one cancellation signal
  • yieldless/router turns 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.

On this page