result-async
v1.3.0
Published
Error handling
Downloads
34
Maintainers
Readme
Result-Async
A library for handling errors with types and without exceptions - even asynchronous ones.
result-async
focuses on making a few things easier in JavaScript/TypeScript:
- Predictable error handling for synchronous and asynchronous functions
- Handling errors in pipelines of synchronous and asynchronous functions
- Making pipelines easy to read
- Giving friendly names to proven functional programming concepts
Install
npm i result-async pipeout --save
# or
yarn add result-async pipeout
Then import the functions and (in TypeScript) types you need:
import { ok, okThen, okChainAsync, Result } from "result-async";
import { pipeA } from "pipeout";
Useful addition: a pipe functions
result-async
is designed to work with a pipe
function. Ramda has pipe and pipeP functions, and Lodash has flow. But pipe
and flow
don't handle promise-returning functions, and pipeP
won't handle adding a non-promise function to the middle of your pipeline.
For a typesafe pipe
that handles async functions, you can use this author's pipeout. pipeout.pipeA
works like a normal pipe
, but every function you add to the pipeline is partially applied, and you use .value
to get the final result. That turns
c(await b(a(await x)));
into
pipe(x)(a)(b)(c).value;
It has a few other varients of pipe
, you can see the pipeout docs for more information.
Install pipeout
If you want to use pipeout, you just have to install and import it.
npm i result-async pipeout --save
# or
yarn add result-async pipeout
import { pipeA } from "pipeout";
Examples
Here are some example of what working with result-async
looks like.
For more examples, and complete documentation, see
the docs.
Full Example
result-async
helps you handle errors in a controlled, readable, and typesafe way,
using techniques from functional programming.
Here's an example of the sort of data-processing pipelines you can build:
async function countAllComments(postsCache: PostsCache): Promise<number> {
// prettier-ignore
return pipeA
(promiseToResult(postsCache.getCache()))
(errorDo(console.error))
(errorRescueAsync(fetchPosts))
(okDo(logPostCount))
(okDoAsync(postsCache.updateCache))
(okThen(R.map(fetchCommentsForPost)))
(okChainAsync(allOkAsync))
(okThen(R.map(comments => comments.length)))
(okThen(R.sum))
(okDo(logCommentTotal))
(okOrThrow)
.value;
}
The basic premise is functions that start with ok
will only operate on successful data,
and pass on error messages.
Functions that start with error
will only operate on error messages, and either change
or try to rescue them back into successes.
Prefixing the function names makes it easy to scan a pipeline and find the error handling code.
Other functions transform other data structures to Result
s (like promiseToResult
),
or handle collections of Result
s (like allOkAsync
).
Functions ending in async
will return a Promise
that resolves to a Result
. This helps
TypeScript track types, and helps developers differentiate between pure and impure functions.
See ./src/full-example for a full working example.
To run the example, run:
yarn ts-node -O '{"module": "commonjs"}' src/full-example.ts
Techniques
React to errors inline
import { isError } from "result-async";
const result = await tryToGetAListOfComments("my-blog-post");
const comments = isOk(result) ? result.ok : [];
Return a success or failure status
import { ok, error } from "result-async";
return isAllWell() ? ok("all is well") : error("all is lost");
Kick off asynchronous calls without waiting for a response
import { fetchUser, errorDo, errorRescueAsync } from "result-async";
import { pipeA } from "pipeout";
// prettier-ignore
pipeA
(fetchUser())
(errorDo(sendErrorToLoggingSystem))
(errorRescueAsync(logOutUser))
);
Transform promise functions into result functions
import { resultify, errorThen, okChainAsync } from "result-async";
import { pipeA } from "pipeout";
const fetchResult = resultify(fetch);
// prettier-ignore
pipeA
(someUrl)
(fetchResult)
(errorThen(transformError))
(okChainAsync(anotherAsyncFunction))
Return standard promises to interop with other functions
import { okChain, errorReplace } from "result-async";
import { pipeA } from "pipeout";
function doStuff(): Promise<SomeData> {
// prettier-ignore
return pipeA(
(someResultFunction)
(okChain(validateData))
(errorReplace("something went wrong"))
(okOrThrow)
}
Typescript
result-async
is written in TypeScript, and it's built to help write typesafe error handling code. But it works great with vanilla JavaScript too!
One of the big benefits of ResultP
, a Promise
of a Result
, is that Promise
s don't have type information about their error case. But many promises have predictable error types. So, you can have your asynchronous functions return a ResultP
to declare those errors types in the type system.
Also ResultP
s should always resolve and never be rejected. This means no more try/catch
blocks, even when using async/await
.
Types
The important types are:
type Result<OkData, ErrorMessage> = OkResult<Data> | ErrorResult<Message>;
type ResultP<OkData, ErrorMessage> = Promise<Result<OkData, ErrorMessage>>;
So a Result
could be either Ok or an Error - and either way, the payload is strongly typed.
Guards
isOk(result)
and
isError(result)
are both typeguards, so if isOk
returns true, typescript will know the Result is actually an OkResult:
function(result: Result<number, string>) {
// Type error: ok could be undefined.
result.ok + 1
if (isOk(result)) {
// No type error, ok is definitely defined.
result.ok + 1
}
}
Testing with Result-Async
If you're using a library like Jest for testing, you can generally follow your testing library's advice for testing asynchronous code.
Unlike with standard promises, you don't have to worry about errored ResultP
s throwing errors.
You'll probably want to test if your calls succeed or fail. If you want to check both the result and payload, try matching against another result
:
import { ok } from "result-async";
it("should be fine", async () => {
expect(await myResultyFunction()).toEqual(ok("all is well"));
});
If you only want to check if a call succeeded for failed, you can just check for the presence of "ok" or "error".
it("should be fine", async () => {
expect(await myResultyFunction()).toHaveProperty("ok");
});
For more complicated checks you can use okOrThrow/Error
to throw an error if the
result isn't what you expect, and otherwise extract the payload for further testing. For example:
expect(okOrThrow(await myResultyFunction())).toContain("is well");
Background
Async Error Handling
async
/await
has been a huge win for avoiding the dreaded Pyramid of doom in JavaScript.
These bad days are behind us forever:
someFunction(data, (err, data) => {
if (err) throw err;
secondFunction(data, (err, data) => {
if (err) throw err;
thirdFunction(data, (err, data) => {
if (err) throw err;
doStuff(data);
});
});
});
When we can now use async/await
to do this:
try {
data = await someFunction(data);
data = await secondFunction(data);
data = await thirdFunction(data);
} catch (e) {
doSomethingWithError(e);
}
Much better. But still, errors are treated really differently than normal data. And in TypeScript, errors are also untyped. For a genuine exception that's fine, but there are a lot of cases where I expect a call to sometimes fail (for instance, a request to an endpoint that could 404). In those cases, having to rely on catching errors can feel a little heavyweight.
The functional programming world has an answer for this - the Result type. It's in Haskell, Ocaml, Elixir, and more. The idea is to have a function that could fail return a response that's marked as either a Success or a Failure. Then you can react to that result status, pass it along, ignore it, or whatever - just like any other data structure.
In many FP languages, we have pattern matching to make this easier:
case some_async_function() do
{:ok, data} -> do_something(data)
{:error, msg} -> do_something_else(msg)
end
We don't have that in JavaScript, but result-async
tries to make it easy to create and handle Result
s - even when they come from async functions:
import { either } from "result-async";
either(
await someAsyncFunction(),
data => doSomething(data),
msg => doSomethingElse(msg)
);
What about Fantasy-Land
If you come from a Haskell-y background, you might be saying, "hey, Result
is just an Either
type, and okThen
is just map
". You're right! And if you're looking for more abstract functional programming, you may be interested in libraries like Sanctuary or Folktalk, which provide a Fantasy-Land compatible Either and Result types, respectively. Fluture is also a great tool for Fantasy-Land compatible asynchronous programming.
But if your team isn't ready that all that, think of Result-Async
like a gateway drug for full ADT-style programming. It lets you write composable, functional programs, but with functions names that are trying to be friendlier to people who don't think in monands.
Publishing Updates
yarn docs
npm version patch | minor | major
npm publish