a simple, discriminated union of a failure and a result
- failure-or
- ErrorOr The best library ever! The original C# implementation of this library!
Loving the project? Show your support by giving the project a star!
Checkout auto generated typedoc here!
npm install failure-or
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),
);
Internally, the FailureOr
object has a list of Failure
s, 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));
}
}
From a value
const result: FailureOr<number> = FailureOr.fromValue(5);
From a Failure
const result: FailureOr<number> = FailureOr.fromFailure(Failure.unexpected());
From multiple Failure
s
const result: FailureOr<number> = FailureOr.fromFailures([
Failure.unexpected(),
Failure.validation(),
]);
From a value
const result: FailureOr<number> = ok(5);
From a Failure
const result: FailureOr<number> = fail(Failure.unexpected());
From multiple Failure
s
const result: FailureOr<number> = fail([
Failure.unexpected(),
Failure.validation(),
]);
const result: FailureOr<User> = User.create();
if (result.isFailure) {
// result contains one or more failures
}
const result: FailureOr<User> = User.create();
if (result.isSuccess) {
// result is a success
}
const result: FailureOr<User> = User.create();
if (result.isSuccess) {
// the result contains a value
console.log(result.value);
}
const result: FailureOr<User> = User.create();
if (result.isFailure) {
result.failures // contains the list of failures that occurred
.forEach((failure) => console.error(failure.description));
}
const result: FailureOr<User> = User.create();
if (result.isFailure) {
const firstFailure = result.firstFailure; // only the first failure that occurred
console.error(firstFailure.description);
}
const result: FailureOr<User> = User.create();
if (result.isFailure) {
result.failuresOrEmptyList; // one or more failures
} else {
result.failuresOrEmptyList; // empty list
}
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.
const foo: string = result.match(
(value) => value,
(failures) => `${failures.length} errors occurred`,
);
const foo: string = await result.matchAsync(
(value) => Promise.resolve(value),
(failures) => Promise.resolve(`${failures.length} errors occurred`),
);
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.
const foo: string = result.matchFirst(
(value) => value,
(firstFailure) => firstFailure.description,
);
const foo: string = await result.matchFirstAsync(
(value) => Promise.resolve(value),
(firstFailure) => Promise.resolve(firstFailure.description),
);
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.
result.switch(
(value) => console.log(value),
(failures) => console.error(`${failures.length} errors occurred`),
);
await result.switchAsync(
(value) =>
new Promise((resolve) => {
console.log(value);
resolve();
}),
(failures) =>
new Promise((resolve) => {
console.error(`${failures.length} errors occurred`);
resolve();
}),
);
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.
result.switchFirst(
(value) => console.log(value),
(firstFailure) => console.error(firstFailure.description),
);
await result.switchFirstAsync(
(value) =>
new Promise((resolve) => {
console.log(value);
resolve();
}),
(firstFailure) =>
new Promise((resolve) => {
console.error(firstFailure);
resolve();
}),
);
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
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()),
);
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}`),
);
Each Failure
instance has a type
property, which is a string that represents the type of the error.
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',
);
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
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);
}
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);
}
If you have any questions, comments, or suggestions, please open an issue or create a pull request 🙂
This project is licensed under the terms of the MIT license.