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
esresult
?
What is 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
) ornull
(String.match
) to indicate an error, - you don't need to fallback to
let
just to use a variable assigned from within atry/catch
closure.
esresult
exist?
Why does 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 | 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.
esresult
instead!
Using 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
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.
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
).
import Result from "esresult";
function fn(): Result<string> {
return Result(value);
}
const [value] = fn();
^ // string
And 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.)
esresult
work?
How does 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,
- which will always have a
- An "Error" Result,
- which will always have a non-
undefined
.error
property, - and does not have a
.value
property, 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
never
is given forResult
'sValue
parameter, only a union of "Error" is produced. - Vice versa, if
never
is given forResult
'sError
parameter, 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
meta
type 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
.error
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.
- 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 esresult
Usage
With no errors
- A simple function that returns a
string
without any defined errors.
import Result from "esresult";
function fn(): Result<string> {
return Result("string");
}
- Because the
Result
signature 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
Result
may be an error, as determined by its.error
property.
... 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");
undefined
on error
... use value, or - Or you may default to
undefined
in the case of an error.
const valueOrUndefined = fn().orUndefined();
throw
on error
... use value, or - 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 of theResult
object itself.
- Don't use
const value = fn().orThrow();
... use value, after handling error
- You can use the
Result
object 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
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.
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
meta
property with the correct type, you will need to discriminate by.error.type
first.
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.Async
as 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
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.
- 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.fn
to wrap unsafe functions (includingasync
functions) thatthrow
.- The return type of the wrapped function is correctly inferred as the
Value
of the Result return signature. - If the function
throw
s, 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
.parse
and.stringify
methods are frequently used, soesresult
offers a pre-wrapped drop-inJSON
object 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
import
statement, before all other imports and application code. - This declares global TypeScript typings and adds
Result
toglobalThis
.
// 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.