DEV Community

druchan
druchan

Posted on

Piping through functions that can throw an exception - Javascript Recipe

I love the pipe function. It's insanely useful in function composition and data transformation workflows where you send data through a long list of functions.

I wont go into the details and intricacies of the pipe function but the gist is best described with this example:

Suppose you want to do all these steps in sequence:

  • take a number
  • add 1 to the result
  • then double that result
  • then square the result of doubling
  • then find the square-root of the previous result
  • then halve it
  • and finally decrement it

The example is trite (and you end up with the same number) but if you had a simple pipe function, you could write it like so:

//              the input number
const doSteps = (number) =>
  pipe(
    // assume that all these named functions are defined somewhere
    inc1, // a => a + 1
    double, // a => a * 2
    square, // a => a * a
    root, // basically Math.sqrt
    halve, // a => a / 2
    dec1 // a => a - 1
  )(number);

doSteps(2); // 2
Enter fullscreen mode Exit fullscreen mode

However, real-life scenarios are hardly as ideal or simple.

You deal with functions that could potentially throw an error and that kind of throws a spanner in the works. Unless you are okay with your pipe composition to throw (because one of the functions in the pipe did), it's not very neat. And I don't like runtime exceptions in my app so I use this handy-little exception-free pipe.

const safeWrapper = (func, value) => {
  if (value.err) {
    return { err: value.err };
  }
  if (value.data) {
    try {
      let res = func(value.data);
      return { data: res };
    } catch (e) {
      return { err: e };
    }
  }
};
const pipe =
  (...fns) =>
  (initialInput) => {
    return fns.reduce(
      (acc, fn) => {
        return safeWrapper(fn, acc);
      },
      { data: initialInput }
    );
  };
Enter fullscreen mode Exit fullscreen mode

What's happening here is that I'm not calling the functions in the pipe and using their results directly. Instead, I'm wrapping them – sort of like a Faraday's box – so that if the functions throw an error, they are contained.

A small change here is that the final output is of the form:

{ data: any | undefined, err: Err | undefined}
Enter fullscreen mode Exit fullscreen mode

As an example:

let inc1 = (a) => a + 1;
let double = (a) => a * 2;
let square = (a) => a * a;
let root = Math.sqrt;
let halve = (a) => a / 2;
let dec1 = (a) => a - 1;
pipe(inc1, double, square, root, halve, dec1)(2); // { data: 2 };
Enter fullscreen mode Exit fullscreen mode

But if one or any of the functions throw:

let err = new Error('error when squaring');
let inc1 = (a) => a + 1;
let double = (a) => a * 2;
let square = (a) => {
  throw err; // instead of doing the job, we throw an error
};
let root = Math.sqrt;
let halve = (a) => a / 2;
let dec1 = (a) => a - 1;
pipe(inc1, double, square, root, halve, dec1)(2); // { err: Error('error when squaring') };
Enter fullscreen mode Exit fullscreen mode

The first function to throw will (kind of) "short-circuit" the whole operation and that will be the error returned.

SurveyJS custom survey software

JavaScript UI Libraries for Surveys and Forms

SurveyJS lets you build a JSON-based form management system that integrates with any backend, giving you full control over your data and no user limits. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs