npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pvorona/failable

v0.10.1

Published

Typed success/failure results for expected failures in TypeScript.

Readme

@pvorona/failable

Typed success/failure results for expected failures in TypeScript.

Use @pvorona/failable when failure is part of normal control flow: invalid input, missing config, not found, or a dependency call that can fail. Return a Failable<T, E> instead of throwing, then handle the result explicitly.

A Failable<T, E> is either Success<T> or Failure<E>.

  • success() / success(data) / failure() / failure(error) create results
  • failable(...) captures thrown or rejected boundaries
  • run(...) composes multiple Failable steps
  • all(...), allSettled(...), and race(...) combine multiple sources
  • result.map(...) / result.mapError(...) / result.flatMap(...) transform and chain results

Install

npm i @pvorona/failable

This package is ESM-only and requires Node 18+.

Basic Usage

Return success(...) or failure(...), then branch on result.isFailure. The typed error lets the caller decide what to do for each failure reason.

import { failure, success, type Failable } from '@pvorona/failable';

type ReadPortError = { code: 'missing' } | { code: 'invalid'; raw: string };

function readPort(raw: string | undefined): Failable<number, ReadPortError> {
  if (raw === undefined) return failure({ code: 'missing' });

  const port = Number(raw);
  if (!Number.isInteger(port) || port <= 0) {
    return failure({ code: 'invalid', raw });
  }

  return success(port);
}

const result = readPort(process.env.PORT);

if (result.isFailure) {
  switch (result.error.code) {
    case 'missing':
      console.error('PORT is not set');
      break;
    case 'invalid':
      console.error(`PORT is not a valid number: ${result.error.raw}`);
      break;
  }
} else {
  console.log(`Listening on ${result.data}`);
}

Choose The Right API

| Need | Use | | ------------------------------------------------------- | ---------------------------------------------------------------------------- | | Return a successful or failed result from your own code | success(...) / failure(...) | | Read the value or provide a fallback | getOr(...) / getOrElse(...) | | Recover to Success<T> | or(...) / orElse(...) | | Map both branches to one output | match(onSuccess, onFailure) | | Throw an Error from a failure | getOrThrow(toError?) / throwIfFailure(result, toError?) | | Capture a throwing or rejecting boundary | failable(...) | | Compose multiple Failable steps | run(...) | | Combine multiple Failable sources | all(...), allSettled(...), race(...) | | Transform a successful value only | map(...) | | Transform a failure value only | mapError(...) | | Chain another Failable step | flatMap(...) | | Cross a structured-clone boundary | toFailableLike(...) + failable(...) | | Validate unknown input | isFailable(...), isSuccess(...), isFailure(...), isFailableLike(...) |

Unwrapping And Recovery

Start with ordinary branching on result.isFailure or result.isSuccess. When you want something shorter, use the helper that matches the job:

  • result.getOr(fallback): return the success value or an eager fallback
  • result.getOrElse(() => fallback): lazy fallback
  • result.getOrElse((error) => fallback): lazy fallback derived from the failure
  • result.or(fallback): recover to Success<T> with an eager fallback
  • result.orElse(() => fallback): lazy recovery to Success<T>
  • result.orElse((error) => fallback): lazy recovery to Success<T> derived from the failure
  • result.match(onSuccess, onFailure): map both branches to one output
  • result.getOrThrow(toError?): return the success value or throw an Error derived from the failure
  • throwIfFailure(result, toError?): throw an Error derived from the failure and narrow the same variable

Both throw helpers preserve existing Error instances unchanged by default. Other failure values become:

  • new Error(\Expected value to be Success. Received Failure(${String(reason)}).`, { cause: reason })`
  • or new Error('Expected value to be Success. Received Failure(Unstringifiable error value).', { cause: reason }) when string coercion itself throws

Pass toError when you need a specific throwable shape at the throw boundary. You can pass either:

  • a callback that returns an Error or a string message
  • or a string message directly

Use the lazy forms when the fallback is expensive or has side effects. Failure callbacks receive the stored error, so () => ... can ignore it and (error) => ... can use it:

const port = result.getOrElse((error) => {
  return error.code === 'missing' ? 3000 : 8080;
});

Using readPort from above:

const result = readPort(process.env.PORT);

const port = result.getOr(3000);
const label = result.match(
  (port) => `Listening on ${port}`,
  (error) => `Using default port (${error.code})`
);

throwIfFailure narrows the result to Success in place, so subsequent code can access .data without branching:

import { throwIfFailure } from '@pvorona/failable';

const result = readPort(process.env.PORT);

throwIfFailure(result);
console.log(result.data * 2);

When you want a specific Error shape only at the throw site, pass toError there:

const port = readPort(process.env.PORT).getOrThrow(
  (reason) => new Error(`Invalid port (${reason.code})`)
);

Transform And Chain With map(...), mapError(...), And flatMap(...)

Use result.map(fn) when you only need to change the success value. The callback runs on Success only; on Failure, the same failure is returned unchanged.

Use result.mapError(fn) when you only need to change the failure value. The callback runs on Failure only; on Success, the same success is returned unchanged.

Use result.flatMap(fn) when the next step can fail again. The callback must return another Failable. On Success, that result becomes the outcome; on Failure, flatMap short-circuits and keeps the original error.

Building on readPort from Basic Usage:

import { failure, success, type Failable } from '@pvorona/failable';

type ReadPortError = { code: 'missing' } | { code: 'invalid'; raw: string };

type ApplicationPortError =
  | ReadPortError
  | { code: 'not_application_port'; port: number };

function readPort(raw: string | undefined): Failable<number, ReadPortError> {
  if (raw === undefined) return failure({ code: 'missing' });

  const port = Number(raw);
  if (!Number.isInteger(port) || port <= 0) {
    return failure({ code: 'invalid', raw });
  }

  return success(port);
}

function ensureApplicationPort(
  port: number
): Failable<number, ApplicationPortError> {
  if (port < 3000 || port > 3999) {
    return failure({ code: 'not_application_port', port });
  }

  return success(port);
}

const appPortResult = readPort(process.env.PORT).flatMap((port) =>
  ensureApplicationPort(port)
);

const labelResult = appPortResult.map(
  (port) => `Application listening on ${port}`
);

const invalidRangeCode = readPort('8080')
  .flatMap((port) => ensureApplicationPort(port))
  .mapError((error) => error.code);

When you pass object literals directly into success(...) or failure(...), TypeScript often keeps their types as narrow as possible (literal fields where that makes sense), which helps switch on error.code and similar patterns.

Capture Thrown Or Rejected Failures With failable(...)

Use failable(...) at a boundary you do not control. It turns a thrown or rejected value into Failure, so the rest of your code can stay in normal Failable flow.

Use the callback form for synchronous code that can throw:

import { failable } from '@pvorona/failable';

const rawConfig = '{"theme":"dark"}';
const configResult = failable(
  () => JSON.parse(rawConfig),
  (reason) => ({ code: 'invalid_config', cause: reason })
);

if (configResult.isFailure) {
  console.error(configResult.error.code, configResult.error.cause);
} else {
  console.log(configResult.data);
}

If you do not pass a second argument, the thrown or rejected value stays in the failure channel unchanged as unknown.

Pass a promise directly when you want rejection capture:

import { failable } from '@pvorona/failable';
import { readFile } from 'node:fs/promises';

const fileResult = await failable(readFile('config.json', 'utf8'), {
  code: 'config_read_failed',
});

const config = fileResult.getOr('{}');

failable(...) can:

  • preserve an existing Failable
  • rehydrate a FailableLike
  • capture sync throws from a callback
  • capture promise rejections from a promise passed directly
  • map captured reasons with toReason
  • store a constant reason with a non-function second argument

By default, the thrown or rejected value becomes .error unchanged.

Pass the promise itself when you want rejection capture. If you call failable(() => promise), only synchronous throws from the callback itself are captured. The returned promise stays in the success channel.

Compose Existing Failable Steps With run(...)

Use run(...) when each step already returns Failable and you want to write the success path once. If any yielded step fails, that failure becomes the default unwind result. Cleanup still runs first, and an explicit return reached in finally overrides it. Yielded cleanup Failure values keep the current unwind result unless a later cleanup return overrides it.

Inside a run(...) builder, there are two valid delegation forms:

  • yield* result when result is already a hydrated Failable
  • yield* await promisedResult in async builders when you have a Promise<Failable<...>>

Hydrated Failable values expose sync and async iterators so run(...) can intercept yield* result in both sync and async builders. Outside run(...), treat them as result objects rather than as a general-purpose collection API.

Without run(...), composing steps means checking each result before continuing:

import { failure, success, type Failable } from '@pvorona/failable';

type ConfigError =
  | { code: 'missing'; key: string }
  | { code: 'invalid'; key: string; raw: string };

function readEnv(
  key: string,
  env: Record<string, string | undefined>
): Failable<string, ConfigError> {
  const raw = env[key];
  if (raw === undefined) return failure({ code: 'missing', key });

  return success(raw);
}

function parsePort(raw: string): Failable<number, ConfigError> {
  const port = Number(raw);
  if (!Number.isInteger(port) || port <= 0) {
    return failure({ code: 'invalid', key: 'PORT', raw });
  }

  return success(port);
}

function loadConfig(
  env: Record<string, string | undefined>
): Failable<{ host: string; port: number }, ConfigError> {
  const hostResult = readEnv('HOST', env);
  if (hostResult.isFailure) return hostResult;

  const rawPortResult = readEnv('PORT', env);
  if (rawPortResult.isFailure) return rawPortResult;

  const portResult = parsePort(rawPortResult.data);
  if (portResult.isFailure) return portResult;

  return success({ host: hostResult.data, port: portResult.data });
}

With run(...), the same flow stays linear:

import { run, success, type Failable } from '@pvorona/failable';

function loadConfig(
  env: Record<string, string | undefined>
): Failable<{ host: string; port: number }, ConfigError> {
  return run(function* () {
    const host = yield* readEnv('HOST', env);
    const rawPort = yield* readEnv('PORT', env);
    const port = yield* parsePort(rawPort);

    return success({ host, port });
  });
}

When a helper already returns a hydrated Failable, yield it directly with yield* helper(). For promised sources in async builders, await them first and then yield the hydrated result with yield* await promisedHelper().

run(...) does not inject helper arguments. Import the top-level combinators you need and use them directly inside the builder.

For async flows, switch to run(async function* ...). Sync hydrated helpers still work with direct yield* helper(), and promised sources compose with yield* await ...:

import {
  all,
  failable,
  failure,
  run,
  success,
  type Failable,
} from '@pvorona/failable';

type ApiError =
  | { code: 'network_error'; cause: unknown }
  | { code: 'http_error'; status: number }
  | { code: 'json_parse_error'; cause: unknown };

type User = { id: string; email: string };
type Profile = { id: string; pictureUrl: string };

async function readJson<T>(url: string) {
  const responseResult = await failable(fetch(url));
  if (responseResult.isFailure) {
    return failure({ code: 'network_error', cause: responseResult.error });
  }

  const response = responseResult.data;
  if (!response.ok) {
    return failure({ code: 'http_error', status: response.status });
  }

  const jsonResult = await failable(response.json());
  if (jsonResult.isFailure) {
    return failure({ code: 'json_parse_error', cause: jsonResult.error });
  }

  return success(jsonResult.data as T);
}

async function getUser(userId: string) {
  return readJson<User>(`https://api.example.com/users/${userId}`);
}

async function getUserProfile(userId: string) {
  return readJson<Profile>(`https://api.example.com/users/${userId}/profile`);
}

async function loadUserPage(
  userId: string
): Promise<Failable<{ user: User; profile: Profile }, ApiError>> {
  return await run(async function* () {
    const [user, profile] = yield* await all(
      getUser(userId),
      getUserProfile(userId)
    );

    return success({ user, profile });
  });
}
  • if a yielded step fails, that failure becomes the default unwind result
  • cleanup still runs, and the last explicit return reached in finally wins
  • yielded cleanup Failure values keep the current unwind result unless a later cleanup return overrides it
  • sync hydrated Failable helpers can use direct yield* helper() in both sync and async builders
  • promised sources in async builders use yield* await promisedHelper()
  • in async builders, use yield* await all(...) to run multiple sources in parallel and get a success tuple or the first failure
  • use yield* all(...) in sync builders when every source is already a hydrated Failable
  • use await allSettled(...) to inspect the settled tuple of sources that resolve to Failable; source promise rejections still reject unchanged
  • use yield* race(...) when every raced source is already a hydrated Failable
  • use yield* await race(...) when any raced source is promised
  • direct promised sources still follow normal async await / try / finally semantics rather than a helper-managed rejection path
  • run(...) does not capture thrown values or rejected promises into Failure; wrap throwing boundaries with failable(...) before they enter run(...)

Parallel Combinators

Import all(...), allSettled(...), and race(...) from the package root when you want to combine multiple sources outside run(...) or inside async builders.

import { all, allSettled, failure, race, success } from '@pvorona/failable';

const syncTuple = all(success(1), success('two'));
const mixedTuple = await all(success(1), Promise.resolve(success('two')));

const settled = await allSettled(
  Promise.resolve(success(1)),
  Promise.resolve(failure('missing-profile'))
);

const syncWinner = race(success('cached'), success('stale'));

const mixedWinner = await race(
  success('cached'),
  Promise.resolve(success('network'))
);

Key semantics:

  • all(...) returns the first failure in input order
  • allSettled(...) returns a plain settled tuple rather than a Success wrapper
  • allSettled(...) preserves Failure values in the returned settled tuple
  • allSettled(...) only settles sources that resolve to Failable values
  • promised source rejections in allSettled(...) still reject the combinator
  • wrap rejecting boundaries with failable(...) first if you want a rejection converted into Failure
  • bare Promise.reject(...) inputs are rejected at type level as a best-effort guardrail; TypeScript still cannot model arbitrary promise rejection channels precisely
  • race(...) accepts sync or promised Failable sources
  • race(...) returns sync Failable when every source is sync, otherwise Promise<Failable>
  • when race(...) mixes already-settled sync and promised sources, winner order follows normal Promise.race(...) input ordering
  • race() with zero sources rejects with a clear error instead of hanging

Transport And Runtime Validation

Failable values are hydrated objects with methods. Keep them inside your process. If you need a structured-clone-friendly shape, convert to FailableLike<T, E> before crossing the boundary and rehydrate on the other side:

import { failure, failable, toFailableLike } from '@pvorona/failable';

const result = failure({ code: 'missing' });

const wire = toFailableLike(result);
const hydrated = failable(wire);

Use the runtime guards only when the input did not come from your own local control flow:

import { isFailable } from '@pvorona/failable';

const candidate: unknown = maybeFromAnotherModule();

if (isFailable(candidate) && candidate.isFailure) {
  console.error(candidate.error);
}
  • use isFailable(...), isSuccess(...), and isFailure(...) for unknown values that might already be hydrated Failable results
  • use isFailableLike(...) for plain transport shapes like { status, data } or { status, error }

API At A Glance

  • type Failable<T, E>: Success<T> | Failure<E>
  • type Success<T> / type Failure<E>: hydrated result variants
  • type FailableLike<T, E>: structured-clone-friendly wire shape
  • success() / success(data) / failure() / failure(error): create hydrated results
  • throwIfFailure(result, toError?) / result.getOrThrow(toError?): throw an Error, preserving existing Error instances unchanged by default
  • failable(...): preserve, rehydrate, capture raw failures, or map them with toReason / a constant reason at a boundary
  • run(...): compose Failable steps without nested branching
  • result.map(...): transform success data; failures pass through unchanged
  • result.flatMap(...): chain another Failable; failures short-circuit
  • toFailableLike(...): convert a hydrated result into a wire shape
  • isFailableLike(...): validate a wire shape
  • isFailable(...), isSuccess(...), isFailure(...): validate hydrated results
  • FailableStatus: runtime success/failure status values