- error- and nothing-handling library for TypeScript
- inspired by Rust's
Result
andOption
types - utilities for composing functions that return errors and interacting with code that throws errors
- no dependencies
CommonJS and ESM modules are available.
npm install @patina/core
import {
Result,
Ok,
Err,
Option,
Some,
AsyncResult,
None,
asyncFn,
Panic,
ErrorWithCause,
} from "@patina/core";
import {db} from "./db";
function divide(a: number, b: number): Result<number, Error> {
if (b === 0) {
return Err(new Error("division by zero"));
}
return Ok(a / b);
}
// You do not have to use `namespace` pattern, but I find it useful to group errors and their mappers together.
namespace DatabaseError {
export class Unreachable extends ErrorWithCause<Error> {
readonly tag = "DatabaseError.Unreachable";
}
export class ValidationError extends ErrorWithCause<Error> {
readonly tag = "DatabaseError.ValidationError";
}
export function from(error: Error): DatabaseError {
if (error.message === "validation error") {
return new ValidationError(error.message, {cause: error});
}
if (error.message === "unreachable") {
return new Unreachable(error.message, {cause: error});
}
// Add more error variants here, for now we panic if we encounter an unknown error
throw new Panic("unhandled database error", {cause: error});
}
}
export type DatabaseError = DatabaseError.ValidationError | DatabaseError.Unreachable;
// Chain API example:
function findGradesByStudentId(id: string): AsyncResult<Option<number[]>, DatabaseError> {
return Result.fromPromise(db.findGradesByStudentId(id))
.map((grades) => (grades ? Some(grades) : None))
.mapErr(DatabaseError.from);
}
// Or you can use `asyncFn` to wrap functions that return `Promise<Result<T, E>>` to convert return type to `AsyncResult<T, E>`
// Inferred type is `(studentId: string) => AsyncResult<number, Error>`
const getAverageGrade = asyncFn(async (studentId: string) => {
const grades = await findGradesByStudentId(studentId)
.andThen((maybeGrades) => {
return maybeGrades.match({
Some: (grades) => {
if (grades.length === 0) {
return Err(new Error("grades not found"));
}
return Ok(grades);
},
None: () => {
// Map to error if grades not found
return Err(new Error("grades not found"));
},
});
})
.mapErr(() => {
// Map to generic error from database error, or handle each variant
return new Error("failed to get grades");
});
// Check and return error
if (grades.isErr()) {
return Err(grades.unwrapErr());
}
// Safe to unwrap because we checked for error
const value = grades.unwrap();
return divide(
value.reduce((a, b) => a + b, 0),
value.length,
);
});
Panic
is a special error that extends native Error
and represents a panic condition. It is used to indicate that a function has failed in an unrecoverable way. The throw
statement should only be used to raise exceptions that represent a bug detected in your program.
On the other hand, Result
can represent either the successful outcome of some operation, Ok<T>
, or an expected runtime error, Err<E>
. Result
types are used alongside user-defined error types that represent the various anticipated runtime failure modes that the associated operation might encounter. Unlike exceptions, Result
types must be explicitly handled and propagated by the developer.
The utilities of this library are designed to differentiate between recoverable errors and panics. For example thrown Panics
will not be caught:
function panicOrError() {
const rand = Math.random();
if (rand > 0.5) {
throw new Panic("random panic");
}
return rand;
}
// Will not catch the panic
const result = Result.from(() => {
panicOrError();
});
Result
is a type that represents either success (Ok
) or failure (Err
).
Result<T, E>
is the type used for returning errors. It is a discriminated union with the variants, Ok<T>
, representing success and containing a value, and Err<E>
, representing error and containing an error value.
Functions return Result
whenever errors are expected and recoverable.
Tries to execute a function and returns the result as a Result
.
const result = Result.from(() => {
if (Math.random() > 0.5) {
return 42;
} else {
throw new Error("random error");
}
});
Tries to resolve a promise and returns the result as a AsyncResult
.
// AsyncResult<number, Error>
const result = Result.fromPromise(Promise.resolve(42));
Returns other
if the result is Ok
, otherwise returns this
(as Err
).
Calls f
if the result is Ok
, otherwise returns this
(as Err
).
let x: Result<number, string> = Ok(2);
assert.deepStrictEqual(x.andThen((n) => Ok(n * 2)).unwrap(), 4);
let y: Result<string, string> = Err("late error");
assert.deepStrictEqual(y.andThen((n) => Ok(n * 2)).unwrapErr(), "late error");
Returns None
if the result is Ok
, otherwise returns Some
containing the error.
Returns the contained Ok
value.
Throws Panic
if the value is an Err
, with a message containing message
and content of the Err
as cause
.
const x = Err("emergency failure");
x.expect("Testing expect"); // throws Panic: Testing expect
Returns the contained Err
value.
Throws Panic
if the value is an Ok
, with a message containing message
and content of the Ok
as cause
.
const x = Ok(2);
x.expectErr("Testing expectErr"); // throws Panic: Testing expectErr
Converts from Result<Result<U, F>, E>
to Result<U, E | F>
.
Calls the provided function with the contained value (if Ok
).
const x = Ok(2);
x.inspect((v) => console.log(v));
Calls the provided function with the contained error (if Err
).
const x = Err("error");
x.inspectErr((e) => console.error(e));
Returns true
if the result is an Err
, and narrows the type to Err
.
Returns true
if the result is an Ok
, and narrows the type to Ok
.
Maps a Result<T, E>
to Result<U, E>
by applying a function to a contained Ok
value, leaving an Err
value untouched.
const x = Ok(10);
const mapped = x.map((n) => `number ${n}`);
assert.strictEqual(mapped.unwrap(), "number 10");
Maps a Result<T, E>
to Result<T, F>
by applying a function to a contained Err
value, leaving an Ok
value untouched.
const x = Err("error");
const mapped = x.mapErr((s) => s.length);
assert.strictEqual(mapped.unwrapErr(), 5);
Returns the provided default (if Err
), or applies a function to the contained value (if Ok
).
const x: Result<string, string> = Ok("foo");
assert.strictEqual(
x.mapOr(42, (v) => v.length),
3,
);
const y: Result<string, string> = Err("bar");
assert.strictEqual(
y.mapOr(42, (v) => v.length),
42,
);
Maps a Result<T, E>
to A | B
by applying fallback function defaultValue
to a contained Err
value, or function f
to a contained Ok
value.
const k = 21;
let x: Result<string, string> = Ok("foo");
assert.strictEqual(
x.mapOrElse(
() => k * 2,
(v) => v.length,
),
3,
);
x = Err("bar");
assert.strictEqual(
x.mapOrElse(
() => k * 2,
(v) => v.length,
),
42,
);
Returns Some
if the result is Ok
, otherwise returns None
.
Returns other
if the result is Err
, otherwise returns this
(as Ok
).
let x: Result<number, string> = Ok(2);
let y: Result<number, string> = Err("late error");
assert.deepStrictEqual(x.or(y).unwrap(), 2);
assert.deepStrictEqual(y.or(x).unwrap(), 2);
Calls f
if the result is Err
, otherwise returns this
(as Ok
).
let x: Result<number, string> = Ok(2);
assert.deepStrictEqual(x.orElse((e) => Err(e + "bar")).unwrap(), 2);
let y: Result<number, string> = Err("foo");
assert.deepStrictEqual(y.orElse((e) => Err(e + "bar")).unwrapErr(), "foobar");
Returns the contained Ok
value or undefined
.
This works differently from Rust's unwrap
method, which panics if the value is an Err
. Since TypeScript compiler is not able to enforce that a value is checked for error before unwrapping, this method is safer to use.
let x: Result<number, string> = Ok(2);
let value = x.unwrap(); // `value` type is `2 | undefined`
if (x.isOk()) {
value = x.unwrap(); // `value` type is `2`
} else {
value = x.unwrap(); // `value` type is `undefined`
}
Returns the contained Err
value or undefined
.
This works differently from Rust's unwrap_err
method, which panics if the value is an Ok
. Since TypeScript compiler is not able to enforce that a value is checked for error before unwrapping, this method is safer to use.
let x: Result<number, string> = Err("failure");
let value = x.unwrapErr(); // `value` type is `"failure" | undefined`
if (x.isErr()) {
value = x.unwrapErr(); // `value` type is `"failure"`
} else {
value = x.unwrapErr(); // `value` type is `undefined`
}
Returns the contained Ok
value or defaultValue
.
let x: Result<number, string> = Ok(2);
assert.strictEqual(x.unwrapOr(0), 2);
let y: Result<number, string> = Err("error");
assert.strictEqual(y.unwrapOr(0), 0);
Returns the contained Ok
value or the result of a function.
let x: Result<number, string> = Ok(2);
assert.strictEqual(
x.unwrapOrElse(() => 0),
2,
);
let y: Result<number, string> = Err("error");
assert.strictEqual(
y.unwrapOrElse(() => 0),
0,
);
Matches a Result<T, E>
to A | B
by applying a matcher object. Similar to Rust's match
statement.
const x: Result<number, string> = Ok(2);
assert.strictEqual(
x.match({
Ok: (v) => v * 2,
Err: (e) => e.length,
}),
4,
);
const y: Result<number, string> = Err("error");
assert.strictEqual(
y.match({
Ok: (v) => v * 2,
Err: (e) => e.length,
}),
5,
);
AsyncResult
is a type that represents either success (Ok
) or failure (Err
) of an asynchronous operation.
It implements the Promise
interface, so you can use it as a drop-in replacement for promises.
The same methods are available on AsyncResult
as on Result
.
Methods on both Result
and AsyncResult
have async
versions that accept a function that return a Promise
, e.g. mapAsync
, inspectAsync
andThenAsync
, etc. These methods are useful for chaining asynchronous operations and will turn a Result
into an AsyncResult
.
Similar methods are available on Option
as on Result
.
AsyncOption
is a type that represents either a value (Some
) or nothing (None
) of an asynchronous operation.
Wraps a function that returns any shape of Promise<Result<any, any>>
and wraps the return value in a AsyncResult
.
// (a: number, b: number) => Promise<Err<string> | Ok<number>>
const divide = async (a: number, b: number) => (b === 0 ? Err("division by zero") : Ok(a / b));
// (a: number, b: number) => AsyncResult<number, string>
const wrapped = asyncFn(divide);
// then you can await the result
const result = await wrapped(1, 2); // => Result<number, string>
Returns true
if value
is an instance of Result
.
Returns true
if value
is an instance of AsyncResult
.
Returns true
if value
is an instance of Option
.
Returns true
if value
is an instance of AsyncOption
.
These utilities help your code appear and behave more like traditional synchronous code by propagating errors automatically.
Note that TS sometimes has trouble inferring yielded result type, especially when a lot of yield*
operations are used in a single function. For complex cases, you may need to explicitly type the result or revert to handling Results
directly.
Runs a generator function that returns a Result
and infers its return type as Result<T, E>
.
yield*
must be used to unwrap and propagate a Result
:
- yielding an
Ok
will unwrap the value - yielding an
Err
will stop the function and return the error
// Result<number, string>
const result = tryFn(function* () {
const a = yield* Ok(1);
if (Math.random() > 0.5) {
yield* Err("error");
}
return a + random;
});
Runs an async generator function that returns a Result
and infers its return type as AsyncResult<T, E>
.
yield*
must be used to unwrap and propagate a Result
:
- yielding an
Ok
will unwrap the value - yielding an
Err
will stop the function and return the error
const okOne = () => new AsyncResult(Promise.resolve(Ok(1)));
// AsyncResult<number, string>
const result = tryAsync(async function* () {
const a = yield* okOne();
if (Math.random() > 0.5) {
yield* Err("error");
}
return a + random;
});
Adding an iterator to the Result class -- to make trySync
and tryAsync
work -- has introduced an unexpected behavior that has an effect on how testing libraries handle deep comparisons of instances of this class.
This may interfere with how deep equality checks are performed, as the tests rely on iterating over object properties or their prototypes to determine equality.
This means asserting equality between any two instances of the Result class will always pass, even if the instances are not equal:
expect(Ok()).toEqual(Ok(1));
expect(Err()).toEqual(Err(1));
expect(Ok()).toEqual(Err());
To properly test equality between instances of the Result class, you can unwrap the value or error and compare it with the expected value directly:
expect(Ok().unwrap()).toEqual(Ok(1).unwrap()); // Now fails as expected
Unfortunately, there are no native TypeScript statements or expression that can help you deal with or propagate errors and you can only go so far with wrapping try
and catch
. Writing code with this library will look very different to what you're used to. Although it may seem verbose, it is more explicit and safer, but on the other hand it may not be straightforward to use and sometimes you can find yourself fighting the type system.
If you find this library lacking, you may want to check out similar libraries: