esresult v3.5.0
Table of Contents
- What is esresult?
- Why does esresult exist?
- How does esresult work?
- Comparison to existing libraries
- Installation
- Usage
- Helpers
- As global definition
- License
What is 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
@throwsjsdoc annotations, - you don't need to write
Errorsubclasses boilerplate, - you don't need to return arbitary values like
-1(Array.findIndex) ornull(String.match) to indicate an error, - you don't need to fallback to
letjust to use a variable assigned from within atry/catchclosure.
Why does esresult exist?
You will be writing a lot of functions.
function fn() {
...
}Your functions will often need to return some kind of value.
function fn(): string {
return value;
}And will probably need to report errors of some kind.
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.
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.
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.
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,
the caller needs to use let outside of the try block to then assign it
from within.
import { fn, NotFoundError } from "./fn";
let value: string | undefined = undefined;
try {
value = fn();
} catch (e) {
if (e instanceof NotFoundError) {
...
}
}
console.log(value);
^ // string | undefinedThis "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.
Using esresult 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.
import Result from "esresult";
function fn(): Result<string, "NotFound" | "DatabaseQueryFailed"> {
if (condition)
return Result.error("NotFound");
if (condition)
return Result.error("DatabaseQueryFailed");
return Result(value);
}- No need to import anything else but the
fnitself. - 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.
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 | undefinedAnd 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).
import Result from "esresult";
function fn(): Result<string> {
return Result(value);
}
const [value] = fn();
^ // stringAnd once you add (or remove) an error case, TypeScript will be able let you know.
import Result from "esresult";
function fn(): Result<string, "Invalid"> {
if (isInvalid)
return Result.error("Invalid");
return Result(value);
}
const [value] = fn();
^ // ? Possible ResultError is not iterable! (You must handle the error case first.)How does esresult 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.errorproperty,
- which will always have a
- An "Error" Result,
- which will always have a non-
undefined.errorproperty, - and does not have a
.valueproperty, therefore an "Ok" Result must be narrowed/discriminated first.
- which will always have a non-
This means that checking for the truthiness of .error will easily
discriminate between "Ok" and "Error" Results.
- If
neveris given forResult'sValueparameter, only a union of "Error" is produced. - Vice versa, if
neveris given forResult'sErrorparameter, only a union of "Value" is produced.
Result is a function that produces an "Ok" Result object, whereby Error
is never.
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
metatype can be defined via a tuple:Result<never, ["MyError", { foo: string }]> - An "Error" Result object can be instantiated similarly:
Result.error(["MyError", { foo: "bar" }]);
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.prototype - "Error" Result object has,
ResultError.prototype->Result.prototype->Object.prototype
The Result.prototype defines methods such as or(), orUndefined(), and
orThrow().
Comparisons
How does esresult compare to other result/error handling libraries?
- Overall
esresult:- is mechanically simple to discriminate on a single
.errorproperty. - 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.
- is mechanically simple to discriminate on a single
| 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 |
Installation
$ npm install esresultUsage
With no errors
- A simple function that returns a
stringwithout any defined errors.
import Result from "esresult";
function fn(): Result<string> {
return Result("string");
}- Because the
Resultsignature has no defined errors the caller doesn't need to handle anything else.
const [value] = fn();With one error
- A function that returns a string or a
"NotFound"error.
function fn(): Result<string, "NotFound"> {
return Result("string");
return Result.error("NotFound");
}- The returned
Resultmay be an error, as determined by its.errorproperty.
... use value, or a default value on error
- You may provide a default value of matching type to the expected value of the Result.
const valueOrDefault = fn().or("default");... use value, or undefined on error
- Or you may default to
undefinedin the case of an error.
const valueOrUndefined = fn().orUndefined();... use value, or throw on error
- Or you may crash your program when in an undefined state that should never
happen (e.g. initialisation code).
- Don't use
.orThrowwith try/catch blocks as this defeats the purpose of theResultobject itself.
- Don't use
const value = fn().orThrow();... use value, after handling error
- You can use the
Resultobject directly to handle specific error cases and create error chains.
const $ = fn();
if ($.error)
return Result.error("FnFailed", { cause: $ })
const [value] = $;With many errors
- You can provide a union of error types to define many possible errors.
function fn(): Result<string, "NotFound" | "NotAllowed"> {
return Result("string");
return Result.error("NotFound");
return Result.error("NotAllowed");
}const $ = fn();
if ($.error) {
$.error.type
^ // "NotFound" | "NotAllowed"
}With detailed errors
- You can add typed
metainformation to allowing callers to parse more from your error.- Provide a tuple with the error type and the meta type/shape to use.
function fn(): Result<
string,
| "NotFound"
| "NotAllowed"
| ["QueryFailed", { query: Record<string, unknown>; }]
> {
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
metaproperty with the correct type, you will need to discriminate by.error.typefirst.
const $ = fn();
if ($.error) {
if ($.error.type === "QueryFailed") {
$.error.meta
^ // { query: Record<string, unknown> }
} else {
$.error.meta
^ // undefined ? Only "QueryFailed" has a meta property definition.
}
}Async functions
- Use
Result.Asyncas a shortcut forPromise<Result>.
async function fn(): Result.Async<string, "Error"> {
return Result("string");
return Result.error("Error");
}- Results are just ordinary objects that are perfectly compatible with async/await control flows.
const $ = await fn();
const value = $.or("default");
const value = $.orUndefined();
if ($.error) {
return;
}
const [value] = $;Chaining errors
- 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
causeproperty 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.
- You can provide a
function main(): Result<string, "FooFailed"> {
const $foo = fn();
^ // ? Returns a Result that may be an error.
if ($foo.error)
return Result.error("FooFailed", { cause: $foo });
return Result(value);
}Wrap throwable functions (.fn)
- Use
Result.fnto wrap unsafe functions (includingasyncfunctions) thatthrow.- The return type of the wrapped function is correctly inferred as the
Valueof the Result return signature. - If the function
throws, the Error is captured in a{ thrown: Error }container.
- The return type of the wrapped function is correctly inferred as the
const parse = Result.fn(JSON.parse);
^ // (text: string, ...) => Result<unknown, Thrown>
const $ = parse(...);
^ // Result<unknown, Thrown>Execute throwable functions (.try)
- 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.
const $ = Result.try(() => {});
^ // Result<void, Thrown>
const $ = Result.try(async () => {});
^ // Result.Async<void, Thrown>
const $ = Result.try(() => JSON.stringify(...));
^ // Result<string, Thrown>Helpers
JSON
- The built-in JSON
.parseand.stringifymethods are frequently used, soesresultoffers a pre-wrapped drop-inJSONobject replacement.- You can achieve the same result with
Result.fn(JSON.parse)etc.
- You can achieve the same result with
import { JSON } from "esresult";
const $ = JSON.parse(...);
^ // Result<unknown, Thrown>
const $ = JSON.stringify(...);
^ // Result<string, Thrown>As global definition
You can top-level import Result as a global type and variable, making Result
feel as if it were a standard language feature, similar to Promise and Date.
This is particularly useful if you don't want to have to import the Result
across all your files.
import "esresult/global"
Simply add import "esresult/global" to the top of your project's entrypoint.
- It should be your first
importstatement, before all other imports and application code. - This declares global TypeScript typings and adds
ResulttoglobalThis.
// index.ts (entrypoint)
+ import "esresult/global";
// your code ...// fn.ts
function fn(): Result<number, "Error"> {
^ // Can now use Result without needing to `import` it.
}License
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.
3 years ago
4 years ago
4 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago