Yieldless
Reference

yieldless/result

Small tuple combinators for readable success and error pipelines.

yieldless/result keeps tuple handling pleasant once a flow has more than one branch. It does not add a runtime, a pipe DSL, or a new result object. Every helper accepts or returns the same [error, value] tuple from yieldless/error.

Exports

  • isOk(result): result is [null, T]
  • isErr(result): result is [E, null]
  • fromNullable(value, createError): SafeResult<NonNullable<T>, E>
  • mapOk(result, mapper): SafeResult<Value, E>
  • mapOkAsync(result, mapper): Promise<SafeResult<Value, E>>
  • mapErr(result, mapper): SafeResult<T, NextError>
  • mapErrAsync(result, mapper): Promise<SafeResult<T, NextError>>
  • andThen(result, mapper): SafeResult<Value, E | NextError>
  • andThenAsync(result, mapper): Promise<SafeResult<Value, E | NextError>>
  • tapOk(result, effect): SafeResult<T, E>
  • tapOkAsync(result, effect): Promise<SafeResult<T, E>>
  • tapErr(result, effect): SafeResult<T, E>
  • tapErrAsync(result, effect): Promise<SafeResult<T, E>>
  • toPromise(result): Promise<T>

Example

import { err, ok, safeTry } from "yieldless/error";
import { andThenAsync, fromNullable, mapOk, tapErr } from "yieldless/result";

const [error, view] = await andThenAsync(
  await safeTry(loadUser(userId)),
  async (user) => {
    const existingUser = fromNullable(
      user,
      () => new Error("User not found"),
    );

    return mapOk(existingUser, (value) => ({
      id: value.id,
      name: value.name,
    }));
  },
);

return tapErr(
  error ? err(error) : ok(view),
  (cause) => logger.warn(cause),
);

Behavior notes

  • Mapping helpers only touch the branch named in the function.
  • Async helpers are explicit so synchronous tuple code can stay synchronous.
  • fromNullable() treats both null and undefined as missing.
  • toPromise() is intended for framework boundaries that require promise rejection.

When to use it

Use these helpers when a tuple flow is becoming noisy but still belongs in ordinary TypeScript. If the code is clearer with one if (error) return [error, null], keep the branch.

Good

Use fromNullable() to turn a common domain miss into an explicit tuple.

const userResult = fromNullable(
  await repository.findUser(id),
  () => new NotFoundError("User not found"),
);

Use andThen() when the second step depends on the first success value.

const result = andThen(userResult, (user) =>
  user.active ? ok(user) : err(new ForbiddenError("User is inactive")),
);

Use tapErr() for logging or telemetry that should not change the result.

return tapErr(result, (error) => {
  logger.warn({ error }, "load user failed");
});

Avoid

Do not turn a simple branch into a clever pipeline.

const result = andThen(
  andThen(await loadUser(id), validateUser),
  renderUser,
);

If the explicit branch is clearer, keep it.

const [error, user] = await loadUser(id);
if (error) return [error, null] as const;

return renderUser(user);

Do not use toPromise() in the middle of tuple-native application code. It is for framework boundaries that require rejection.

On this page