No need to make promises, or Promises vice versa

Each programmer who begins to develop for Node.js faces a choice of strategy for organizing asynchronous code in a project. While it is quite simple to maintain hygiene of asynchronous code in small system utilities, with an increase in the mass of code in a project, solving this problem begins to require the introduction of an additional, so-called control flow tool.

This article will examine the small flow flow library “Flowy” , which is the development of the ideas of Step Tim Caswell’s project, and whose core is based on the CommonJS Promises concepts , as well as arguments why Promises are so inconvenient.




What does it look like


function leaveMessage(username, text, callback) {
    Flowy(
        function() {
            // concurrent execution of two queries
            model.users.findOne(username, this.slot());
            model.settings.findOne(username, this.slot());
        },
        function(err, user, settings) {
            // error propagating
            if (!user) throw new Error('user not found');
            if (!settings.canReceiveMessages) throw new Error('violating privacy settings');
            model.messages.create(user, text, this.slot());
        },
        function(err, message) {
            model.notifications.create(message, this.slot());
        },
        callback //any error will be automatically propagated to this point
    );
}

  • Each step passed to the wrapper Flowyis performed in the context of the library (variable this). At the same time, the context provides the ability to transfer data to the next step by generating callbacks, which can be passed to the classic nodejs-like functions as the last argument (call this.slot()).
  • All that is performed in one step is performed in parallel.
  • Management will be transferred to the next step only after all its “slots” are filled with data - all callbacks generated by the call this.slot()will complete successfully, or the first one will receive an error message.
  • If an error occurs in any of the steps, the entire chain will be interrupted and the error will be returned to the last step.


Why does it look that way?


A programmer who begins to familiarize himself with the Node.js non-blocking I / O subsystem API is offered an asynchronous call interface of the following form:

fs.readFile('/etc/passwd', 'utf8', function (err, data) {
    if (err) throw err;
    console.log(data);
});

When using other people's modules, it would be a natural desire to have an interface similar to the one described above - the rule of least surprise is one of the keys to supported and easily debugged code. This is where the first requirement for the library comes from:

We want to keep the “native” nodejs-like function and callback interfaces. Each step Flowyhas a nodejs callback interface, which makes it easy to wrap the entire chain of steps in a traditional nodejs function.

At the same time, the main idea of ​​Promises ( Chris Cowell's “Q” library will be used as an example of implementation ) is to replace the callback transfer with the last argument in an asynchronous call by creating a chain of calls to Promise methods:

// chaining promises: Q.fcall(step1).then(step2).then(step3).done()
return getUsername()
.then(function (username) {
    return getUser(username)
    .then(function (user) {
        // if we get here without an error, the value returned here
        // or the exception thrown here resolves the promise returned by the first line
    })
})

The first thing that catches your eye: functions return Promise . Thus, to use the library, you need to wrap all the "classic" functions in a Promise adapter (this process is described in more detail on the project page), or develop code with hard-oriented library interfaces (but at the same time, all public module interfaces will need to be brought back to classic look, given the requirement formulated above). It is not comfortable. It sounds scary and looks equally scary. At the same time, the second requirement for the control flow library immediately comes to mind: the

library should only be a “glue” between the existing parts of the system and not become a heavy dependency.All the features of the functioning of “Flowy” are hidden inside the steps - the glue itself - which allows the functions using it to remain “clean” for the outside world. Litter should remain in the hut.

When working with libraries that allow you to create chaining from asynchronous calls, you often need to make part of the calls in parallel . The Q library provides the following awkward solution:

Q.allResolved(promises)
.then(function (promises) {
    promises.forEach(function (promise) {
        if (promise.isFulfilled()) {
            var value = promise.valueOf();
        } else {
            var exception = promise.valueOf().exception;
        }
    })
})

In addition to everything, if we suddenly want to break the rule “one argument - one returned value”, then we will have to do additional exercises:

return getUsername()
.then(function (username) {
    return [username, getUser(username)];
})
.spread(function (username, user) {
})

Reading this code, another requirement for the library suggests itself:

We want to easily execute several parallel queries and pass any number of arguments to the callbacks. Flowy can do this without any extra effort on the part of the developer due to its architecture.

So, “Flowy” is a lightweight library for managing the asynchronous flow of program execution, which makes it easy to solve everyday issues of developers under Node.js and has proven itself in the production environment.

This article only demonstrates the basic features of Flowy. For a more detailed acquaintance, I invite everyone to visit the project page on the github, where you will find abundant documentation with many examples.

Useful sources:

Also popular now: