Skip to main content

a simple, discriminated union of a failure and a result

Works with
This package works with Bun
This package works with Cloudflare Workers
This package works with Node.js
This package works with Deno
This package works with Browsers
JSR Score
76%
Published
a month ago (1.3.0)

failure-or

GitHub Workflow Status (with event) Codecov GitHub License npm

a simple, discriminated union of a failure and a result

Credits

  • ErrorOr The best library ever! The original C# implementation of this library!

Give it a star!

Loving the project? Show your support by giving the project a star!

Getting Started

Checkout auto generated typedoc here!

npm install failure-or

Replace throwing errors with FailureOr<T>

With throwing errors

function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }

  return a / b;
}

try {
  const result = divide(4, 2);
  console.log(result * 2);
} catch (error) {
  console.error(error);
}

With FailureOr<T>

function divide(a: number, b: number): FailureOr<number> {
  if (b === 0) {
    return fail(Failure.unexpected('Divide.ByZero', 'Cannot divide by zero'));
  }

  return ok(a / b);
}

const result = divide(4, 2);
if (result.isFailure) {
  console.error(result.firstFailure.description);
}

console.log(result.value * 2);

Or, using map/else and switch/match methods

divide(4, 2)
  .map((value) => value * 2)
  .switchFirst(
    (value) => console.log(value),
    (failure) => console.log(failure.description),
  );

Return multiple Failures when needed

Internally, the FailureOr object has a list of Failures, so if you have multiple failures, you don't need to compromise and have only the first one

class User {
  private readonly name: string;

  private constructor(name) {
    this.name = name;
  }

  public static create(name: string): FailureOr<User> {
    const failures: Failure[] = [];

    if (name.length < 2) {
      failures.push(
        Failure.Validation('User.Name.TooShort', 'Name is too short'),
      );
    }

    if (name.length > 100) {
      failures.push(
        Failure.Validation('User.Name.TooLong', 'Name is too long'),
      );
    }

    if (name.trim() === '') {
      failures.push(
        Failure.Validation(
          'User.Name.Required',
          'Name cannot be empty or whitespace only',
        ),
      );
    }

    if (failures.length > 0) {
      return fail(failures);
    }

    return ok(new User(name));
  }
}

Creating a FailureOr<T> instance

Using FailureOr methods

From a value

const result: FailureOr<number> = FailureOr.fromValue(5);

From a Failure

const result: FailureOr<number> = FailureOr.fromFailure(Failure.unexpected());

From multiple Failures

const result: FailureOr<number> = FailureOr.fromFailures([
  Failure.unexpected(),
  Failure.validation(),
]);

Using helper functions

From a value

const result: FailureOr<number> = ok(5);

From a Failure

const result: FailureOr<number> = fail(Failure.unexpected());

From multiple Failures

const result: FailureOr<number> = fail([
  Failure.unexpected(),
  Failure.validation(),
]);

Properties

isFailure

const result: FailureOr<User> = User.create();

if (result.isFailure) {
  // result contains one or more failures
}

isSuccess

const result: FailureOr<User> = User.create();

if (result.isSuccess) {
  // result is a success
}

value

const result: FailureOr<User> = User.create();

if (result.isSuccess) {
  // the result contains a value

  console.log(result.value);
}

failures

const result: FailureOr<User> = User.create();

if (result.isFailure) {
  result.failures // contains the list of failures that occurred
    .forEach((failure) => console.error(failure.description));
}

firstFailure

const result: FailureOr<User> = User.create();

if (result.isFailure) {
  const firstFailure = result.firstFailure; // only the first failure that occurred

  console.error(firstFailure.description);
}

failuresOrEmptyList

const result: FailureOr<User> = User.create();

if (result.isFailure) {
  result.failuresOrEmptyList; // one or more failures
} else {
  result.failuresOrEmptyList; // empty list
}

Methods

match

The match method receives two callbacks, onValue and onFailure, onValue will be invoked if the result is a success, and onFailure will be invoked if the result is a failure.

match

const foo: string = result.match(
  (value) => value,
  (failures) => `${failures.length} errors occurred`,
);

matchAsync

const foo: string = await result.matchAsync(
  (value) => Promise.resolve(value),
  (failures) => Promise.resolve(`${failures.length} errors occurred`),
);

matchFirst

The matchFirst method received two callbacks, onValue, and onFailure, onValue will be invoked if the result is a success, and onFailure will be invoked if the result is a failure.

Unlike match, if the state is a failure, matchFirst's onFailure function receives only the first failure that occurred, not the entire list of failures.

matchFirst

const foo: string = result.matchFirst(
  (value) => value,
  (firstFailure) => firstFailure.description,
);

matchFirstAsync

const foo: string = await result.matchFirstAsync(
  (value) => Promise.resolve(value),
  (firstFailure) => Promise.resolve(firstFailure.description),
);

switch

The switch method receives two callbacks, onValue and onFailure, onValue will be invoked if the result is a success, and onFailure will be invoked if the result is a failure.

switch

result.switch(
  (value) => console.log(value),
  (failures) => console.error(`${failures.length} errors occurred`),
);

switchAsync

await result.switchAsync(
  (value) =>
    new Promise((resolve) => {
      console.log(value);
      resolve();
    }),
  (failures) =>
    new Promise((resolve) => {
      console.error(`${failures.length} errors occurred`);
      resolve();
    }),
);

switchFirst

The switchFirst method receives two callbacks, onValue and onFailure, onValue will be invoked if the result is a success, and onFailure will be invoked if the result is a failure.

Unlike switch, if the state is a failure, switchFirst's onFailure function receives only the first failures that occurred, not the entire list of failures.

switchFirst

result.switchFirst(
  (value) => console.log(value),
  (firstFailure) => console.error(firstFailure.description),
);

switchFirstAsync

await result.switchFirstAsync(
  (value) =>
    new Promise((resolve) => {
      console.log(value);
      resolve();
    }),
  (firstFailure) =>
    new Promise((resolve) => {
      console.error(firstFailure);
      resolve();
    }),
);

map

map

map receives a callback function, and invokes it only if the result is not a failure (is a success).

const result: FailureOr<string> = User.create('John').map((user) =>
  ok('Hello, ' + user.name),
);

Multiple map methods can be chained together.

const result: FailureOr<string> = ok('5')
  .map((value: string) => ok(parseInt(value, 10)))
  .map((value: number) => ok(value * 2))
  .map((value: number) => ok(value.toString()));

If any of the methods return a failure, the chain will break and the failures will be returned.

const result: FailureOr<string> = ok('5')
  .map((value: string) => ok(parseInt(value, 10)))
  .map((value: number) => fail<number>(Failure.unexpected()))
  .map((value: number) => ok(value * 2)); // t

mapAsync

else

else

else receives a callback function, and invokes it only if the result is a failure (is not a success).

const result: FailureOr<string> = fail<string>(Failure.unexpected()).else(() =>
  ok('fallback value'),
);
const result: FailureOr<string> = fail<string>(Failure.unexpected()).else(
  (failures) => ok(`${failures.length} errors occurred`),
);
const result: FailureOr<string> = fail<string>(Failure.unexpected()).else(() =>
  fail(Failure.notFound()),
);

elseAsync

Mixing methods (then, else, switch, match)

You can mix then, else, switch and match methods together.

ok('5')
  .map((value: string) => ok(parseInt(value, 10)))
  .map((value: number) => ok(value * 10))
  .map((value: number) => ok(value.toString()))
  .else((failures) => `${failures.length} failures occurred`)
  .switchFirst(
    (value) => console.log(value),
    (firstFailure) =>
      console.error(`A failure occurred : ${firstFailure.description}`),
  );

Failure Types

Each Failure instance has a type property, which is a string that represents the type of the error.

Built in failure types

The following failure types are built in:

export const FailureTypes = {
  Default: 'Default',
  Unexpected: 'Unexpected',
  Validation: 'Validation',
  Conflict: 'Conflict',
  NotFound: 'NotFound',
  Unauthorized: 'Unauthorized',
} as const;

Each failure type has a static method that creates a failure of that type.

const failure = Failure.notFound();

Optionally, you can pass a failure code and description to the failure.

const failure = Failure.unexpected(
  'User.ShouldNeverHappen',
  'A user failure that should never happen',
);

Custom failure type

You can create your own failure types if you would like to categorize your failures differently.

A custom failure type can be created with the custom static method

const failure = Failure.custom(
  'MyCustomErrorCode',
  'User.ShouldNeverHappen',
  'A user failure that should never happen',
);

You can use the Failure.type property to retrieve the type of the failure

Built in result types

There are few built in result types

const result: FailureOr<Success> = ok(Result.success);
const result: FailureOr<Created> = ok(Result.created);
const result: FailureOr<Updated> = ok(Result.updated);
const result: FailureOr<Deleted> = ok(Result.deleted);

Which can be used as following

function deleteUser(userId: string): FailureOr<Deleted> {
  const user = database.findById(userId);
  if (!user) {
    return fail(
      Failure.NotFound('User.NotFound', `User with id ${userId} not found`),
    );
  }

  database.delete(user);

  return ok(Result.Deleted);
}

Organizing Failures

A nice approach, is creating a object with the expected failures.

const DIVISION_ERRORS = {
  CANNOT_DIVIDE_BY_ZERO: Failure.unexpected(
    'Division.CannotDivideByZero',
    'Cannot divide by zero',
  ),
} as const;

Which can later be used as following

function divide(a: number, b: number): FailureOr<number> {
  if (b === 0) {
    return fail(DIVISION_ERRORS.CANNOT_DIVIDE_BY_ZERO);
  }

  return ok(a / b);
}

Contribution

If you have any questions, comments, or suggestions, please open an issue or create a pull request 🙂

License

This project is licensed under the terms of the MIT license.

Add Package

deno add @joyeux/failure-or

Import symbol

import * as mod from "@joyeux/failure-or";

Add Package

npx jsr add @joyeux/failure-or

Import symbol

import * as mod from "@joyeux/failure-or";

Add Package

yarn dlx jsr add @joyeux/failure-or

Import symbol

import * as mod from "@joyeux/failure-or";

Add Package

pnpm dlx jsr add @joyeux/failure-or

Import symbol

import * as mod from "@joyeux/failure-or";

Add Package

bunx jsr add @joyeux/failure-or

Import symbol

import * as mod from "@joyeux/failure-or";