DEV Community

loading...

How to handle a nodeback in ReasonML

Yawar Amin
Programming languages enthusiast. Author of Learn Type Driven Development: https://www.packtpub.com/application-development/learn-type-driven-development
Updated on ・2 min read

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

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

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

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;

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]

Discussion (4)

Collapse
idkjs profile image
Alain

Greetings, sir. I tossed this code into a file and got this error:

  /Users/prisc_000/working/NATIVE/bloody-authenticator/src/NodeBack.re 14:30-33

  12 ┆ nodeback(
  13 ┆   fun
  14 ┆   | Error(err) => raise({j|$err|j}) // Can't access data in this branc
       h
  15 ┆   | Ok(data) => Js.log(data),
  16 ┆ ) // Can't access err in this branch

  This has type:
    string
  But somewhere wanted:
    exn

code i copied:

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

Node.Fs.readFileSync(
  "/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
);

How would I fix that?

Collapse
yawaramin profile image
Yawar Amin Author • Edited

Hi Alain, somehow I completely missed your comment here. Thanks for pointing out this error. Mistake on my part, can be fixed by swapping out the raise with failwith: | Error(err) => failwith({j|$err|j}) // Can't access data in this branch. raise expects something of type exn, but we are calling it with a string. We should use failwith instead which does take a string.

After that fix there is a second error, because the Node.Fs.readFileSync function doesn't actually take a nodeback. In my example I used a hypothetical function Node.Fs.readFile. I say hypothetical because it's not bound in the libraries shipped with BuckleScript, but of course fs.readFile is a real NodeJS function and you could write a binding fairly easily: nodejs.org/dist/latest-v10.x/docs/...

Anyway here's a full working example:

[@bs.module "fs"]
external readFile:
  (
    ~path: string,
    ~options: {
                .
                "encoding": option(string),
                "flag": option(string),
              }
                =?,
    (. Js.Nullable.t('err), Js.Nullable.t(string)) => unit
  ) =>
  unit =
  "";

let nodeback = f =>
  (. err, result) =>
    switch (Js.Nullable.(toOption(err), toOption(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")
    };

let readFile = (~flag=?, ~path, f) =>
  readFile(
    ~path,
    ~options={"encoding": Some("utf8"), "flag": flag},
    nodeback(f),
  );

let test = () =>
  readFile(
    ~path="/etc/passwd",
    fun
    | Ok(data) => Js.log(data) // Can't access err in this branch
    | Error(err) => failwith({j|$err|j}) // Can't access data in this branch
  );
Collapse
idkjs profile image
Alain

Thanks for taking the time, sir. I can't get this to run for whatever reason. I got this gist to run, gist.github.com/idkjs/c48dda9f1dba... but when i got to apply the concept, i keep running into problems. Even searched through your book! I will keep at it. Peace to you.

Thread Thread
yawaramin profile image
Yawar Amin Author

My apologies, I should have tested that example more thoroughly. I've now updated my previous comment with a really working code sample. You can run it like this:

$ bsb
$ node
> const {test} = require('./src/Main.bs')
> test()