How to handle a nodeback in ReasonML

Yawar Amin - Sep 8 '19 - - Dev Community

NODEJS callback-style programming entered the JavaScript developer's toolbox a few years ago and brought with it the term 'nodeback', short for (I guess) 'node callback'. The idea of this callback is that it gets called with upto two arguments: an error value xor a success value, representing that the previous operation failed or succeeded and letting the programmer decide what to do next. For example:

fs.readFile('/etc/passwd', (err, data) => {
  if (err) throw err;
  console.log(data);
});
Enter fullscreen mode Exit fullscreen mode

Although the nodeback style of programming has mostly been superseded in the JavaScript world, thanks to the advent of promises and async/await, developers still occasionally have to deal with it.

The problem with this callback is that either of the parameters might be undefined, and you have to, every time, manually implement the logic of the callback in such a way that the data is not accessed if there is a non-empty err, and vice-versa.

In strongly, statically-typed languages like ReasonML, we have the ability to wrap up this unsafe API, with a slight runtime overhead, in a much more type-safe and ergonomic API. Here's the wrapper:

let nodeback(f) = (. err, result) =>
  switch (err, result) {
  | (Some(err), None) => f(Js.Result.Error(err))
  | (None, Some(result)) => f(Ok(result))
  // Throw if APIs break nodeback 'guarantee':
  | _ => invalid_arg("Nodeback arguments invalid")
  };
Enter fullscreen mode Exit fullscreen mode

You can use this like so (with a hypothetical Node.Fs.readFile binding):

Node.Fs.readFile("/etc/passwd", nodeback(fun
  | Error(err) => raise({j|$err|j}) // Can't access data in this branch
  | Ok(data) => Js.log(data)), // Can't access err in this branch
);
Enter fullscreen mode Exit fullscreen mode

The way nodeback works is, it takes as input a type-safe result-handling function and converts it into a nodeback (formatted to highlight the input and output):

let nodeback:
  (Js.Result.t('a, 'e) => 'b)
  =>
  (. option('e), option('a)) => 'b;

Enter fullscreen mode Exit fullscreen mode

You can use the nodeback wrapper to get its type-safety benefits, while passing the JavaScript side the nodeback that it expects.

[EDIT: see correction and full working example in the comment below]

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .