Eliminate try-catch when you code, AND get access to all 3 outcomes of "throwers".
- 0 dependencies
- 100% test coverage
- 400 bytes minified & compressed
Sneak peak
import { tri } from "tri-fp";
const [error, result] = tri(myFuncThatMightThrow)(arg1, arg2);
if (error) // ...
// continue with result
This module aims to:
- Make both functions and promises more functional by removing the need for try-catch.
- Handle application errors and native exceptions differently.
- Not alter native JS constructs, instead only provide tiny wrappers.
- Increase readability & control.
Wrap regular functions & promises to make it possible to handle all 3 outcomes differently: Results, Native Exceptions, & Application Errors.
+--------+
+--->| Result |
| +--------+
+----------+ |
| Function | | +------------------+
| / |----|--->| Native exception |
| Promise | | +------------------+
+----------+ |
| +-------------------+
+--->| Application error |
+-------------------+
In JavaScript, application errors and native exceptions are mixed and can not be easily separated and handled differently. You should handle the first category gracefully and let the user know, and on the contrast make your actual bugs let you know about them quickly by crashing. Read more about this under "Motivation" below.
npm i tri-fp
Wrap a function in tri
, or a promise in triAsync
, to enable the handling of all 3 outcome categories differently:
- Native exceptions are still thrown.
- The return from the new function/promise will be an error-first pair,
[error, result]
.
import { tri, triAsync } from "tri-fp";
// Sync
const [error, result] = tri(myFunction)(arg1, arg2);
// Async, direct promise
const [error, result] = await triAsync(myPromise);
// Async, promise returning function
const [error, result] = await triAsync(myPromiseReturningFunc)(arg1, arg2);
// Native errors/exceptions will still throw.
// Don't catch these; Find your bug.
// Of course check the error in some way after using one of the above:
if (error) // ...
Sometimes we actually know that native exceptions can be treated as application errors (e.g. JSON.parse
). Thus, a bi
/biAsync
wrapper is also provided ("bi" for splitting to only 2 outcomes instead of 3/"tri") that convert native exceptions to application errors (Error instance). (See "Advanced use" section below to make your own Error conversion).
import { bi, biAsync } from 'tri-fp';
// NOTE: Will not throw native exceptions, but convert them to Error.
// Don't use this if you're not sure why.
const [error, result] = bi(myFunction)(arg1, arg2);
const [error, result] = await biAsync(myPromise);
const [error, result] = await biAsync(myPromiseReturningFunc)(arg1, arg2);
One example where bi
would be useful is handling of dynamic JSON:
import { bi } from 'tri-fp';
const safeParse = bi(JSON.parse);
const [error, result] = safeParse(data);
if (error) {
// ...
}
For completeness (& only 2 extra lines of code in this source) a basic try-catch wrapper is also provided: tryCatch
/tryCatchAsync
that only converts try-catch patterns to error-first pairs. These 2 functions are however not recommended -- you should use one of the above.
import { tryCatch, tryCatchAsync } from 'tri-fp';
// DOUBLE WARNING! This will keep native exceptions as is, but not throw them!
const [error, result] = tryCatch(myFunction)(arg1, arg2);
const [error, result] = await tryCatchAsync(myPromise);
const [error, result] = await tryCatchAsync(myPromiseReturningFunc)(arg1, arg2);
Read about the subject of problems with Promises and why one would avoid using throw, try/catch, etc. in some references below.
- https://dev.to/craigmichaelmartin/the-problem-with-promises-in-javascript-5h46
- https://medium.com/better-programming/js-reliable-fdea261012ee
- https://medium.com/@gunar/async-control-flow-without-exceptions-nor-monads-b19af2acc553
The motivation for 2 different names, tri
& triAsync
is to keep it, or make it, clear that a promise is something different where we need to use e.g. await
. We can then also support promise returning functions, which is a preferred way to create promises anyway (closer to a more functional "task").
It is possible to access the raw wrappers, and provide your own Error transforming function.
import { tryWrap, tryWrapAsync } from 'tri-fp';
const throwAllBut = (errorTypes) => (err) => {
if (errorTypes.includes(err.constructor)) return err;
throw err;
};
const tryFunc = tryWrap(throwAllBut([MyError]));
const [error, result] = tryFunc(myFunction);
// Perhaps error == MyError
Use tryWrapAsync
the same way, and it will work like triAsync
/biAsync
/tryCatchAsync
.
Another additional feature is that it is possible to configure that all empty values instead of undefined
should be some other value, i.e. null
.
import { setNoneValue } from 'tri-fp';
setNoneValue(null);
// All uses of the tri-fp lib will now return null at empty spaces in the error-first pairs.
If you want to use the even more fp style Either type, here is an example with sanctuary
.
import * as S from 'sanctuary';
import { tri as baseTri } from 'tri-fp';
// arrayPairToEither :: (Array2 Error a) -> (Either Error a)
const arrayPairToEither = ([e, r]) => (e ? S.Left(e) : S.Right(r));
// tri :: (*... -> a) -> (*... -> (Either Error a))
const tri = (f) => S.unchecked.pipe([baseTri(f), arrayPairToEither]);
// Now use tri as described above
{
"plugins": ["plugin:functional/no-exceptions"]
}
Avoid using try-catch
or .catch
.
Hippocratic 2.1