tri-fp

Eliminate try-catch and make it possible to handle all 3 outcomes differently: result, native exception, & application error.

Usage no npm install needed!

<script type="module">
  import triFp from 'https://cdn.skypack.dev/tri-fp';
</script>

README

tri-fp 🔱

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 |
                     +-------------------+

What? Why?

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.

Get Started

npm i tri-fp

API

tri & triAsync

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) // ...

bi & biAsync

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';

// WARNING! 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 saferParse = bi(JSON.parse);
const [error, result] = saferParse(data);

if (error) {
  showSnackbar(`Corrupt data package: ${error.message}`);
  console.warn(error);
}

tryCatch & tryCatchAsync

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);

Motivation

Read about the subject of problems with Promises and why to avoid using throw, try/catch, etc. in some references below.

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").

Advanced use

Custom Error Transform

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.

Empty Value Configuration

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.

Use with Either

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

Recommended eslint

eslint-plugin-fp eslint-plugin-functional

{
  "plugins": ["fp"],
  "rules": {
    "fp/no-throw": ["warn"]
  }
}

Avoid using try-catch or .catch.

Inspired by / Thanks to

Licence

Hippocratic 2.1