FrenchMajesty FrenchMajesty - 8 months ago 34
Node.js Question

Nodejs callbacks to methods or promises?

I am new to Node and I saw a callback hell pattern appear in my application which makes it hard to read.

After some research the internet is giving 2 main solutions:

-Export out the functions

turn this:

var fs = require('fs');

var myFile = '/tmp/test';
fs.readFile(myFile, 'utf8', function(err, txt) {
if (err) return console.log(err);

txt = txt + '\nAppended something!';
fs.writeFile(myFile, txt, function(err) {
if(err) return console.log(err);
console.log('Appended text!');
});
});


into this:

var fs = require('fs');

function notifyUser(err) {
if(err) return console.log(err);
console.log('Appended text!');
};

function appendText(err, txt) {
if (err) return console.log(err);

txt = txt + '\nAppended something!';
fs.writeFile(myFile, txt, notifyUser);
}

var myFile = '/tmp/test';
fs.readFile(myFile, 'utf8', appendText);


and
use Promises

I am leaning more toward the exports of function but the Internet is saying that promises are the better alternative to handling async calls.

I do not want to get into something to later have to change my coding habits/style to match standard convention, its better to start on the right path.

So should I just start using promises or is the exporting of function is a fine solution?

Answer Source

You have a third option now which may take this out of the realm of pure opinion: Promises + async/await:

ES2017 (the spec coming out in June) will feature async/await, which provide simpler syntax for consuming promises in simple use cases, and NodeJS already supports them in the current version of Node v7 (v7.7.2 as of this writing).

With promises and async/await, your code would look like this:

const p = require(/*...some theoretical promisifier...*/).promisifier;
const fs = require('fs');

async function go() {
    const myFile = '/home/tjc/temp/test';  
    let txt = await p(fs.readFile, myFile, 'utf8');
    txt = txt + '\nAppended something!';
    await p(fs.writeFile, myFile, txt);
    console.log('Appended text!');
}

go().catch(error => {
    console.log(error);
});

It's still asynchronous, it's just that the syntax for simple promise use cases is simpler, allowing you to have the code reflect the logic without intermediary then callback functions.

Notice that you can only use await from within an async function (since they manage promises behind the scenes). Also note that NodeJS is going to get uppity about unhandled promise rejections soon, hence making sure we do the catch on go().

I believe the above converts roughly to the following:

const p = require(/*...some theoretical promisifier...*/).promisifier;
const fs = require('fs');

function go() {
    const myFile = '/home/tjc/temp/test';  
    return p(fs.readFile, myFile, 'utf8').then(txt => {
        txt = txt + '\nAppended something!';
        return p(fs.writeFile, myFile, txt).then(() => {
            console.log('Appended text!');
        });
    });
}

go().catch(error => {
    console.log(error);
});

...but of course if you were writing it yourself, you'd organize it differently:

const p = require(/*...some theoretical promisifier...*/).promisifier;
const fs = require('fs');

function go() {
    const myFile = '/home/tjc/temp/test';  
    return p(fs.readFile, myFile, 'utf8')
        .then(txt => {
            txt = txt + '\nAppended something!';
            return p(fs.writeFile, myFile, txt);
        })
        .then(() => {
            console.log('Appended text!');
        });
}

go().catch(error => {
    console.log(error);
});

As you can see, in terms of clarity, async/await bring a lot to the table for simple use cases. (For more complex use cases, you still have to fall back to explicit promise handling.)

Whether that takes us out of the realm of opinion, though, is another question. And naturally, keeping your functions small and composable is a good thing, whether you're using promises or not.


About p in the above: There are various libs out there for promisifying APIs written using Node's standard callback mechanism; in the above I'm using a theoretical one. Here's a very, very, very simple implementation of p above:

const p = (f, ...args) => {
    return new Promise((resolve, reject) => {
        args.push((err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
        f(...args);
    });
};
exports.promisifier = p;

...but you can find libs that take a more thorough approach.