A tiny, TypeScript-first, result/error handling utility.
npm install esresult

- What is esresult?
- Why does esresult exist?
- Using esresult instead!
- How does esresult work?
- Comparison to existing libraries
- Installation
- Usage
- With no errors
- With one error
- With many errors
- With detailed errors
- Async functions
- Chaining errors
- Wrap throwable functions (.fn)
- Execute throwable functions (.try)
- Helpers
- JSON
- As global definition
- License
esresult?esresult (ECMA-Script Result) is a tiny, zero-dependency, TypeScript-first,
result/error utility.
It helps you easily represent errors as part of your functions' signatures so
that:
- you don't need to maintain @throws jsdoc
annotations,
- you don't need to write Error subclasses
boilerplate,
- you don't need to return arbitary values like -1
(Array.findIndex)
or null
(String.match)
to indicate an error,
- you don't need to fallback to let just to use a variable assigned from
within a try/catch closure.
esresult exist?You will be writing a lot of functions.
``ts`
function fn() {
...
}
Your functions will often need to return some kind of value.
`ts`
function fn(): string {
return value;
}
And will probably need to report errors of some kind.
`ts`
function fn(): string {
if (condition)
throw new Error("NotFound");
return value;
}
You will probably have many different types of errors, so you make subclasses of
Error.
`ts
class NotFoundError extends Error {}
class DatabaseQueryFailedError extends Error {}
function fn(): string {
if (condition)
throw new NotFoundError();
if (condition)
throw new DatabaseQueryFailedError();
return value;
}
`
Traditionally, you will use throw to report error; and it would be best to
document this behaviour somehow.
`ts
class NotFoundError extends Error {}
class DatabaseQueryFailedError extends Error {}
/**
* @throws {NotFoundError} If the record can't be found.
* @throws {DatabaseQueryError} If there is an error communicating with the database.
* @throws {FooError} An error we forgot to remove from the documentation many releases ago.
*/
function fn(): string {
if (condition)
throw new NotFoundError();
if (condition)
throw new DatabaseQueryFailedError();
return value;
}
`
If the caller wants to act conditionally for a particular error we also need
to import those error classes for comparison.
`ts
import { fn, NotFoundError } from "./fn";
try {
const value = fn();
} catch (e) {
if (e instanceof NotFoundError) {
...
}
}
`
If the value returned by fn() (from within the try block) is needed later,let
the caller needs to use outside of the try block to then assign it
from within.
`ts
import { fn, NotFoundError } from "./fn";
let value: string | undefined = undefined;
try {
value = fn();
} catch (e) {
if (e instanceof NotFoundError) {
...
}
}
console.log(value);
^ // string | undefined
`
This "simple" function:
- needs too much boilerplate code to express errors,
- needs the caller to read the docs to learn of possible error behaviour so
that it may safely handle these error-cases,
- needs the caller to litter their code with let & try/catch blocks to
properly scope returned values,
- needs the caller to perform additional imports of error subclasses just to
compare error instances,
- AND, if the function adds (or removes) error behaviour, **static analysis will
not notice**.
instead!What if we could instead reduce all this into something smaller and more
human-friendly with esresult?
- No error subclasses needed, and are now part of the function's signature.
`ts
import Result from "esresult";
function fn(): Result
if (condition)
return Result.error("NotFound");
if (condition)
return Result.error("DatabaseQueryFailed");
return Result(value);
}
`
- No need to import anything else but the fn itself.
- No complications with let + try/catch to handle a particular error.
- All error types can be seen via intellisense/autocompletion.
- Ergonomically handle error cases and default value behaviours.
`ts
import { fn } from "./fn"
const $value = fn();
^ // ? The Result object that may be of Value or Error.
if ($value.error?.type === "NotFound") {
^ // "NotFound" | "DatabaseQueryFailed" | undefined
}
const value = $value.orUndefined();
^ // string | undefined
`
And if the function doesn't have any known error cases yet (as part of its
signature), you can access the successful value directly, without needing to
check error (it will always be undefined).
`ts
import Result from "esresult";
function fn(): Result
return Result(value);
}
const [value] = fn();
^ // string
`
And once you add (or remove) an error case, TypeScript will be able let you
know.
`ts
import Result from "esresult";
function fn(): Result
if (isInvalid)
return Result.error("Invalid");
return Result(value);
}
const [value] = fn();
^ // ? Possible ResultError is not iterable! (You must handle the error case first.)
`
work?esresult default exports Result, which is both a Type and a Function, as
explained below.
Result is a type generic that accepts Value and Error type parameters
to create a discriminable
union
of:
- An "Ok" Result,
- which will always have a undefined .error property,undefined
- An "Error" Result,
- which will always have a non- .error property,.value
- and does not have a property, therefore an "Ok" Result **must
be narrowed/discriminated first**.
This means that checking for the truthiness of .error will easily
discriminate between "Ok" and "Error" Results.
- If never is given for Result's Value parameter, only a union ofnever
"Error" is produced.
- Vice versa, if is given for Result's Error parameter, only a
union of "Value" is produced.
Result is a function that produces an "Ok" Result object, whereby Errornever
is .
Result.error is a function that produces an "Error" Result object, whereby
Value is never.
"Error" Result's can also contain .meta data about the error (e.g. current
iteration index/value, failed input string, etc.).
- An Error's meta type can be defined via a tuple: ResultResult.error(["MyError", { foo: "bar" }]);
- An "Error" Result object can be instantiated similarly:
esresult works with simple objects as returned by Result and Result.error,
of which follow a simple prototype chain:
- "Ok" Result object has, Result.prototype -> Object.prototypeResultError.prototype
- "Error" Result object has, -> Result.prototype ->Object.prototype
The Result.prototype defines methods such as or(), orUndefined(), andorThrow().
How does esresult compare to other result/error handling libraries?
- Overall esresult:.error
- is mechanically simple to discriminate on a single property.
- supports a simple (and fully typed) error shape mechanism that naturally
supports auto-completion.
- supports causal chaining out-of-the-box so you don't need to use another
library.
- relies on simple functions (or, orUndefined, etc) to reduce value-mapping
complexity in favour of native TypeScript control flow.
| | esresult | neverthrow | node-verror | @badrap/result | type-safe-errors | space-monad | typescript-monads | monads | ts-pattern | boxed |
| ---------------------------------- | ------------------- | ------------------------------------------------------ | ---------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------------------------------------- | ----------------------------------------- |
| Result discrimination | .error | .isOk() .isErr() | N/A | .isOk .isErr | as inferred | .isOk() .isResult($) | .isOk() .isErr() | .isOk() .isErr() | as inferred | .isOk() .isErr() |
| Free value access if no error def. | YES | No | N/A | No (must always discriminate; for errors too!) | YES | No | No | No | YES | No |
| Error shapes (type/meta) | YES | No | YES | No (forces of type Error) | No (encourages error instances) | No | No | No | No | No |
| Error causal chaining | YES | No | YES | No | No | No | No | No | No | No |
| Error type autocomplete | YES | No | No (relies on throwing) | No | YES (standard inferred) | No | No | No | YES (standard inferred) | No |
| Wrap unsafe functions | YES | YES | N/A | No | No | No | No | No | N/A | No |
| Execute one-off unsafe functions | YES | No | N/A | No | No | No | No | No | N/A | No |
| Async types | YES | YES | N/A | No | No | No | No | No | N/A | No |
| Wrap unsafe async functions | YES | YES | N/A | No | No | No | No | No | N/A | No |
| value access | or, orUndefined | map, mapErr, orElse (not type restricted) | N/A | unwrap (could throw if not verbose) | map, mapErr | map, orElse | unwrap unwrapOr | unwrap (throws), unwrapOr | N/A | match (not type restricted) |
| orThrow (panic) | YES | No | N/A | " | No | No | No | No | YES, (exhaustive) | No |
`bash`
$ npm install esresult
- A simple function that returns a string without any defined errors.
`ts
import Result from "esresult";
function fn(): Result
return Result("string");
}
`
- Because the Result signature has no defined errors the caller doesn't need
to handle anything else.
`ts`
const [value] = fn();
- A function that returns a string or a "NotFound" error.
`ts`
function fn(): Result
return Result("string");
return Result.error("NotFound");
}
- The returned Result may be an error, as determined by its .error property.
- You may provide a default value of matching type to the expected value of the
Result.
`ts`
const valueOrDefault = fn().or("default");
- Or you may default to undefined in the case of an error.
`ts`
const valueOrUndefined = fn().orUndefined();
- Or you may **crash your program when in an undefined state that should never
happen** (e.g. initialisation code).
- Don't use .orThrow with try/catch blocks as this defeats the purpose ofResult
the object itself.
`ts`
const value = fn().orThrow();
- You can use the Result object directly to handle specific error cases and
create error chains.
`ts
const $ = fn();
if ($.error)
return Result.error("FnFailed", { cause: $ })
const [value] = $;
`
- You can provide a union of error types to define many possible errors.
`ts`
function fn(): Result
return Result("string");
return Result.error("NotFound");
return Result.error("NotAllowed");
}
`ts
const $ = fn();
if ($.error) {
$.error.type
^ // "NotFound" | "NotAllowed"
}
`
- You can add typed meta information to allowing callers to parse more from
your error.
- Provide a tuple with the error type and the meta type/shape to use.
`ts`
function fn(): Result<
string,
| "NotFound"
| "NotAllowed"
| ["QueryFailed", { query: Record
> {
return Result("string");
return Result.error("NotFound");
return Result.error("NotAllowed");
return Result.error(["QueryFailed", { query: { a: 1, b: 2 } }])
^ // ? Providing a tuple that matches the definition's shape.
}
- To access the meta property with the correct type, you will need to.error.type
discriminate by first.
`ts
const $ = fn();
if ($.error) {
if ($.error.type === "QueryFailed") {
$.error.meta
^ // { query: Record
} else {
$.error.meta
^ // undefined ? Only "QueryFailed" has a meta property definition.
}
}
`
- Use Result.Async as a shortcut for Promise.
`ts`
async function fn(): Result.Async
return Result("string");
return Result.error("Error");
}
- Results are just ordinary objects that are perfectly compatible with
async/await control flows.
`ts
const $ = await fn();
const value = $.or("default");
const value = $.orUndefined();
if ($.error) {
return;
}
const [value] = $;
`
- Often you need will have a function calling another function that could also
fail, upon which the caller will fail also.
- You can provide a cause property to your returned error that will begin to
form an error chain of domain-specific errors.
- Error chains are more useful than a traditional stack-traces because they
are specific to your program's domain rather than representing an
programming error resulting in undefined program behaviour.
`ts
function main(): Result
const $foo = fn();
^ // ? Returns a Result that may be an error.
if ($foo.error)
return Result.error("FooFailed", { cause: $foo });
return Result(value);
}
`
- Use Result.fn to wrap unsafe functions (including async functions) thatthrow
.Value
- The return type of the wrapped function is correctly inferred as the throw
of the Result return signature.
- If the function s, the Error is captured in a { thrown: Error }
container.
`ts
const parse = Result.fn(JSON.parse);
^ // (text: string, ...) => Result
const $ = parse(...);
^ // Result
`
- A shortcut method for Result.fn(() => {})(); offers a simple replacement for
a try/catch block.
- Accepts a function with no arguments and immediately invokes it and
forwards its return value (if any) as a Result.
`ts
const $ = Result.try(() => {});
^ // Result
const $ = Result.try(async () => {});
^ // Result.Async
const $ = Result.try(() => JSON.stringify(...));
^ // Result
`
- The built-in JSON .parse and .stringify methods are frequently used, soesresult
offers a pre-wrapped drop-in JSON object replacement.Result.fn(JSON.parse)
- You can achieve the same result with etc.
`ts
import { JSON } from "esresult";
const $ = JSON.parse(...);
^ // Result
const $ = JSON.stringify(...);
^ // Result
`
You can top-level import Result as a global type and variable, making ResultPromise
feel as if it were a standard language feature, similar to and Date.import
This is particularly useful if you don't want to have to the Result
across all your files.
Simply add import "esresult/global" to the top of your project's entrypoint.
- It should be your first import statement, before all other imports andResult
application code.
- This declares global TypeScript typings and adds to globalThis.
`diff
// index.ts (entrypoint)
+ import "esresult/global";
// your code ...
`
`ts
// fn.ts
function fn(): Result
^ // Can now use Result without needing to import it.``
}
Copyright (C) 2022 Peter Boyer
esresult is licensed under the MIT License, a short and simple
permissive license with conditions only requiring preservation of copyright and
license notices. Licensed works, modifications, and larger works may be
distributed under different terms and without source code.