0

The Q library (https://github.com/kriskowal/q) provides very helpful adaptors for functions that follow Node's standard callback layout, i.e. last argument is function(err, result).

return Q.nfcall(FS.readFile, "foo.txt", "utf-8");
return Q.nfapply(FS.readFile, ["foo.txt", "utf-8"]);

That's discussed further in the "Adapting Node" section of the README.

When using native ES6 Promises to accomplish the same, one often ends up with this unwieldy kind of trainwreck:

const fs = require('fs');
const http = require('http');

const server = http.createServer((req, res) => {
    new Promise((resolve, reject) => { 
        fs.readFile('/etc/motd', (err, data) => {
            if(err) { 
                reject(err.toString());
                return;
            }

            resolve(data);
        });
    }).then(data => {
        res.writeHead(200);
        res.end(data);
    }).catch(e => {
        res.writeHead(500);
        res.end(e);
    });
}).listen(8000);

While this does flatten worst-case callback hell, it's still visually cluttered and hard to follow.

Obviously, one could decompose this into functions and inline less code to make it more readable, but that solution works fairly well for rectifying the very callback hell that promises are supposed to help solve in the first place. :-)

Is there anything I'm missing about the standard ES2015/6 Promise feature set that could allow one to save some mess here? Failing that, suggestions for low-calorie polyfills would be appreciated.

Alex Balashov
  • 3,218
  • 4
  • 27
  • 36
  • 1
    The ES6 promises are not meant as an improvement to fix pre-existing libraries that use callbacks, but to be used as a standard alternative in new libraries or new versions. That being said, you can easily promisify node either yourself as Alex mentions, or using a third party – Ovidiu Dolha May 30 '17 at 11:11
  • Maybe consistent indentation would help? But no, I don't think it's unwieldy or difficult to follow, especially when you factor out the `new Promise` thing in a helper method like you already suggested. Just do it. – Bergi May 30 '17 at 11:14
  • The main point of promises is [being returnable values](https://stackoverflow.com/a/22562045/1048572), which makes them much easier to decompose into functions than callbacks. – Bergi May 30 '17 at 11:17
  • Fixed the indentation, sorry! – Alex Balashov May 30 '17 at 11:28
  • I've updated the answer for Node 8 that was released right at the same time when the answer was posted. Guess this answers the question about native way to do this (at least native for Node). – Estus Flask May 31 '17 at 11:17

3 Answers3

3

Browsers and Node.js 7 and lower

Most popular callback-based packages have their promisified counterparts, e.g. fs-extra and mz/fs for fs.

pify is widely known solution in for promisification that uses native Promise by default. Other promise-related packages from this maintainer can be helpful as well, for example p-event to promisify one-time event listeners.

http example involves a callback that is triggered multiple times, something that cannot be replaced with a promise-based function. But it's obviously possible to promisify callback-based things beforehand like fs (as shown in pify-fs package):

const pify = require('pify');
const fs = pify(require('fs'), {
  exclude: [/^exists/, /.+(Stream|Sync)$/, /watch/],
  excludeMain: true
});
...
http.createServer((req, res) => {
    let code;
    let body;

    fs.readFile('/etc/motd')
    .then(
      data => {
        body = data;
        code = 200;
      },
      err => {
        body = String(err);
        code = 500;
      }
    )
    .then(() => {
      res.writeHead(code);
      res.end(body);
    });
})

Without third-party promisification solution the developer is forced to reinvent the wheel, this involves promise construction with new Promise, like shown in original example.

It should be noticed that Bluebird is a popular alternative to ES6 promises particularly because it provides demanded features out of the box, including promisification.

Node.js 8

As of 8.0.0, Node has built-in util.promisify to promisify Node-style callbacks. A recipe for batch promisification of a plain object like fs is

const util = require('util');
const fs = Object.assign({}, require('fs'),
    Object.entries(require('fs'))
    .filter(([, val]) => typeof val === 'function')
    .filter(([key]) => !/^[A-Z_]|^exists|.+(Stream|Sync)$|watch/.test(key))
    .reduce((fs, [key, val]) => Object.assign(fs, { [key]: util.promisify(val) }), {})
);
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • You can just use [doasync](https://www.npmjs.com/package/doasync) npm module with Node 8. It's like pify, but uses **util.promisify**, **Proxy** and **WeakMaps** (for memoization). – Do Async Oct 12 '17 at 21:58
  • @DoAsync Ha, I guess that's the act of self-promotion, but thanks, good to know. I'm not sure if it's a good idea to use proxies for that. Node is known for its speed, and plain p-fication allows to save most of it, while proxies are known for being slow. Have you got some benchmarks for this thing? The problem is see with current implementation is that it p-fies all methods, something that I have never actually done because the exceptions are always there. – Estus Flask Oct 12 '17 at 22:17
  • I have no benchmarks, but it should be faster than **pify**, I guess, because it's too simple and uses memoization. It would be great if someone did the perfomance comparison (I'm also interested). – Do Async Oct 12 '17 at 22:24
  • @DoAsync I will let you know if I'll manage to do some tests. But it's generally always a good thing to consider benchmarks by design in preformance critical libs. I can't say that current pify implementation is bloated. On the contrary, it got rid of pinkie-promise and is [quite neat](https://github.com/sindresorhus/pify/blob/master/index.js). You can notice that most of the bloat comes from argument parsing, which should perform on par with `...args` in Node 8, more or less. And *all* of the options it accepts are very useful, I'd suggest to borrow some ideas from there. – Estus Flask Oct 12 '17 at 22:36
0

I guess one can always invent one's own ...

const fs = require('fs');
const http = require('http');

function promiseAdapt(func, ...args) {
    return new Promise((resolve, reject) => {
        func.apply(this, args.concat((err, res) => {
            if(err) {
                reject(err.toString());
                return;
            }

            resolve(res || undefined);
        }));
    });
}

const server = http.createServer((req, res) => {
    let data = undefined;

    promiseAdapt(fs.readFile, '/etc/motd', { 
        encoding: 'utf8' 
    }).then(d => {
        data = d;

        let str = `${req.method} ${req.url} (${req.headers['user-agent'] || '?'}) from ${req.connection.remoteAddress}` + "\n";

        return promiseAdapt(fs.writeFile, 'access.log', str, {
            encoding: 'utf8',
            flag: 'a',
            mode: '0755'
        });
    }).then(() => {
        res.writeHead(200);
        res.end(data);
     }).catch(e => {
        res.writeHead(500);
        res.end(e);
    });
}).listen(8000);
Alex Balashov
  • 3,218
  • 4
  • 27
  • 36
  • 1
    Yes, this is exactly what I meant with 'reinventing the wheel' in my answer. Then you find out that there's a need to promisify all object methods and provide some options to define the rules how this is going to be done... and you end up with yet another NPM p-fication library in no time. – Estus Flask May 30 '17 at 11:54
0

You can avoid promises altogether and just execute your code synchronously via nsynjs. Your code will transform as follows:

Step 1. Wrap slow functions with callbacks into nsynjs-aware wrappers:

// wrappers.js

var fs=require('fs');
exports.readFile = function (ctx,name) {
    console.log("reading config");
    var res={};
    fs.readFile( name, "utf8", function( error , data){
        if( error ) res.error = error;
        res.data = data;
        ctx.resume(error);
    } );
    return res;
};
exports.readFile.nsynjsHasCallback = true;

Step 2: Write your logic as if it was synchronous, and put it into function:

const synchronousCode = function(req,res,wrappers) {
    try {
        var data = wrappers.readFile(nsynjsCtx,'/etc/motd').data;
        res.writeHead(200);
        res.end(data);
    }
    catch(e) {
        res.writeHead(500);
        res.end(e);
    };
}

Step 3. Execute that function via nsynjs:

// index.js
const fs = require('fs');
const http = require('http');
const nsynjs = require('nsynjs');
const wrappers = require('./wrappers');

const synchronousCode = function(req,res,wrappers) {
    ...
};

const server = http.createServer(function(req, res){
    nsynjs.run(synchronousCode,{},req,res,wrappers,function(){
        console.log('synchronousCode is done');
    })
}).listen(8000);

Please see similar example here https://github.com/amaksr/nsynjs/tree/master/examples/node-module-loading

amaksr
  • 7,555
  • 2
  • 16
  • 17
  • @Bergi, nsynjs has it's own state machine (with own local vars, closures, stacks, etc for each pseudo-thread) and own event loop. It does not block main event loop (well, it does, but it does it the same way as native JS code - only during computations, not while waiting for callbacks) – amaksr May 30 '17 at 15:48
  • So basically, it's async/await from the ES5 era? :-) – Alex Balashov May 30 '17 at 15:59
  • @AlexBalashov yes, except you don't have to specify async/await throughout the code, and with few other features – amaksr May 30 '17 at 16:05
  • @amaksr And that's exactly the feature that makes it horrible. – Bergi May 30 '17 at 16:53
  • @Bergi nsynjs checks for type of evaluated functions during runtime. Depending on type (native, wrapper with callback inside, another synchronous function), it will evaluate it accordingly and suspend execution if necessary. It is similar to async/await approach, but marking of functions is done differently - via setting properties to function objects. – amaksr May 30 '17 at 17:55
  • 1
    @amaksr I'm not claiming that it doesn't work (though apparently it could do better on syntax support, which other approaches have no problem with), I say that not marking *calls* to asynchronous functions is horrible. – Bergi May 30 '17 at 17:58