failure-or

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.

Generated using TypeDoc