esresult

Result monad with useful error handling features.

Usage no npm install needed!

<script type="module">
  import esresult from 'https://cdn.skypack.dev/esresult';
</script>

README

esresult

esresult is a zero-dependency, TypeScript-first utility for better error-handling patterns in your code by making domain-specific errors an explicit part of a function's public API.

By forcing function callers to deal with possible domain-specific errors you can build more transparent and resilient functionality, and circumvent a codebase littered with try/catch blocks or other emergent anti-patterns like brittle catch-all blocks that handle too much.

Returning errors (instead of throwing them) allows the caller to be aware of all possible domain-specific error values of a function, and allows for static type-checking to help enforce correctness of error-handling logic.

esresult supports:

  • intellisense of a Result's possible error types.
  • cause-chaining to build domain-specific error causal-chains.
  • built-in data structure for error messages and info for canonical error descriptions and/or contextual error information (e.g. data of a failing iteration in a loop).
  • ok result with warnings for idiomatic handling of partial success cases where a valid value can be returned despite some non-critical errors.

API

Install

yarn add esresult

Overview

Annotate your functions with a Result generic to explicitly define a successful Ok type (returned with ok(value)) and optionally define all possible domain-specific Err types that the function may return (returned with err(type)).

import { Result, Ok, ok, Err, err, fromThrowable } from "esresult";

Define only Ok type.

function foo(...): Result<number>
function foo(...): Result<Ok<number>>
function foo(...): Result<Result.Ok<number>>

You may use number instead of Ok<number> as optional shorthand. You may also access Ok from Result via. Result.Ok.

Or define both Ok and all possible Err types as a union.

function foo(...): Result<number, "INVALID" | "TOO_BIG">
function foo(...): Result<number, Err<"INVALID" | "TOO_BIG">>
function foo(...): Result<number, Result.Err<"INVALID" | "TOO_BIG">>
function foo(...): Result<number, Err<"INVALID"> | Err<"TOO_BIG">>
function foo(...): Result<number, Result.Err<"INVALID"> | Result.Err<"TOO_BIG">>

Similar to Ok shorthand, you may pass a type instead of Err<type>. You may also access Err from Result via. Result.Err.

Or also define a common info object for errors.

function foo(...): Result<number, "INVALID", { foo: string }>
function foo(...): Result<number, Err<"INVALID", { foo: string }>>
function foo(...): Result<number, Result.Err<"INVALID", { foo: string }>>

Err can be directly provided with the info shape instead of using Result for shorthand.

Or define both Ok, and different info objects per Err types.

function foo(...): Result<
  number,
  | Err<"INVALID", { a?: string }>
  | Err<"TOO_BIG" | "TOO_SMALL", { min?: number, max?: number }>
  | Err<"UNKNOWN", { a: string, b: string }>
>

Use ok(...) and err(...) to return values and errors.

function foo(source: string): Result<number, "INVALID" | "TOO_BIG"> {
  const result = parseInt(source, 10);

  if (Number.isNan(result))
    return err("INVALID");

  if (result > 100)
    return err("TOO_BIG").$info({ max: 100 });

  return ok(result);
}

Read a Result's success or failure state, using .ok.

const $a = foo("100");

// if `ok` is false, Result is an error.
if (!$a.ok) return ...

// otherwise, Result is an ok value.
const a = $a.value;

Create an Error chain, using .$cause(...).

Rather than wrapping many statements in their own try/catch closures (which are annoying when trying to use const for assignments), you can handle returned Result objects and their values directly. Err objects support .$cause(Err) to allow domain-space casual-chaining of errors that make debugging and reporting a breeze.

const $a = foo("100");
if (!$a.ok) {
  // return a new error, and track its cause
  return err("FOO_ERROR").$cause($a);
}
const a = $a.value;

Or continue with default value, using .or(...) and .orUndefined().

Many libraries opt to simplify their API by returning undefined (or null) when encountering an error rather than throwing or otherwise reporting details of a failure. With a Result the API caller can choose to handle an error with undefined or with a value of the matching Ok type.

const a = foo("100").orUndefined(); // default to undefined
const a = foo("100").or(50); // default to different number
const a = foo("100").or("50"); // ts: error: "50" is not of type: number

Or handle a specific error type, using .error.

const $a = foo("100");
if ($a.error === "FOOBAR") {
//               ^ ts: error: can only be: "INVALID" | "TOO_BIG"
  return err("CRITICAL_ERROR").$cause($a);
}
const a = $a.orUndefined(); // gracefully continue

Enrich your Errors, using .$info(...) and .$message(...).

All other Result/error-handling libraries only support a basic error primitive (e.g. string, Error-object, etc.) leaving the developer to implement their own interfaces to store possible contextual information (e.g. status codes, failing object, etc.). Because these use-cases are so common, esresult's Err object supports adding this information out-of-the-box.

const $ = err("QUERY_ERROR")
  .$cause($response) // details on the cause, e.g. network error?
  .$info({ url: requestUrl, query, variables }) // relevant context details
  .$message("Unable to communicate with the server.") // human readable

$.cause
//      ^ type: Err<unknown, unknown> (causal chains are not generic)
$.info.
//      ^ intellisense: "url" | "query" | "variables"
$.message
//        ^ type: string

Ok with partial errors, using .ok.

function foo(...): Result<Ok<number, Err<>>>

Sometimes it is useful to provide a successful value AND output any errors or warnings that are non-critical (e.g. parsing many items, and returning successful items as an array, but reporting failed items as an array of errors).

For convenience, if the given array of errors is empty, .partialErrors will evaluate as undefined. This allows for expressions like !$.partialErrors to check for any partial errors, instead of the needless verbose !$.partialErrors?.length.

const okItems: FooItem[] = [];
const itemWarnings: Err<"INVALID" | "OUT_OF_RANGE">[] = [];

const $ = ok(okItems).$warnings(itemErrors);
const $ = ok(okItems, { warnings: itemErrors });

$.value
//    ^ type: FooItem[]
$.warnings
//         ^ type: undefined | Err<"INVALID" | "OUT_OF_RANGE">[]

Wrap a function that can throw, using .fromThrowable(...).

import { fromThrowable } from "esresult";

// throwable
function fn() { throw new Error(...); }

// wrap
const safeFn = fromThrowable(fn);

// safely call with Result
const $result = safeFn(...);

// thrown error is available as `$result.error`
if (!$result.ok) return err(...).$cause($result);

Fallback to unstructured Error, using err.primitive(...).

If you're unable to use fromThrowable to wrap a throwing function, or you just really don't want to use a structured error type, you can create an primitive err using anything for its error value.

// primitive/unstructured err
const $ = err.primitive(new TypeError(...));

$.ok      // false
$.error   // TypeError

Motivation

Heavily inspired by: