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 🙏

© 2024 – Pkg Stats / Ryan Hefner

failure-or

v1.3.0

Published

a simple, discriminated union of a failure and a result

Downloads

8

Readme

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.