David David - 26 days ago 6
Node.js Question

Using Promises with fs.readFile in a loop

I'm trying to understand why the below promise setups don't work.

(Note: I already solved this issue with async.map. But I would like to learn why my attempts below didn't work.)

The correct behavior should be: bFunc should run as many time as necessary to fs read all the image files (bFunc below runs twice) and then cFunc console prints "End".

Thanks!

Attempt 1: It runs and stops at cFunc().

var fs = require('fs');

bFunc(0)
.then(function(){ cFunc() }) //cFunc() doesn't run

function bFunc(i){
return new Promise(function(resolve,reject){

var imgPath = __dirname + "/image1" + i + ".png";

fs.readFile(imgPath, function(err, imagebuffer){

if (err) throw err;
console.log(i)

if (i<1) {
i++;
return bFunc(i);
} else {
resolve();
};

});

})
}

function cFunc(){
console.log("End");
}


Attempt 2:
In this case, I used a for-loop but it executes out of order. Console prints: End, bFunc done, bFunc done

var fs = require('fs');

bFunc()
.then(function(){ cFunc() })

function bFunc(){
return new Promise(function(resolve,reject){

function read(filepath) {
fs.readFile(filepath, function(err, imagebuffer){
if (err) throw err;
console.log("bFunc done")
});
}

for (var i=0; i<2; i++){
var imgPath = __dirname + "/image1" + i + ".png";
read(imgPath);
};

resolve()
});
}


function cFunc(){
console.log("End");
}


Thanks for the help in advance!

Answer

So, anytime you have multiple async operations to coordinate in some way, I immediately want to go to promises. And, the best way to use promises to coordinate a number of async operations is to make each async operation return a promise. The lowest level async operation you show is fs.readFile(). Since I use the Bluebird promise library, it has a function for "promisifying" a whole module's worth of async functions.

var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs'));

This will create new parallel methods on the fs object with an "Async" suffix that return promises instead of use straight callbacks. So, there will be an fs.readFileAsync() that returns a promise. You can read more about Bluebird's promisification here.

So, now you can make a function that gets an image fairly simply and returns a promise whose value is the data from the image:

 function getImage(index) {
     var imgPath = __dirname + "/image1" + index + ".png";
     return fs.readFileAsync(imgPath);
 }

Then, in your code, it looks like you want to make bFunc() be a function that reads three of these images and calls cFunc() when they are done. You can do that like this:

var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs'));

 function getImage(index) {
     var imgPath = __dirname + "/image1" + index + ".png";
     return fs.readFileAsync(imgPath);
 }

 function getAllImages() {
    var promises = [];
    // load all images in parallel
    for (var i = 0; i <= 2; i++) {
        promises.push(getImage(i));
    }
    // return promise that is resolved when all images are done loading
    return Promise.all(promises);
 }

 getAllImages().then(function(imageArray) {
    // you have an array of image data in imageArray
 }, function(err) {
    // an error occurred
 });

If you did not want to use Bluebird, you could manually make a promise version of fs.readFile() like this:

// make promise version of fs.readFile()
fs.readFileAsync = function(filename) {
    return new Promise(function(resolve, reject) {
        fs.readFile(filename, function(err, data){
            if (err) 
                reject(err); 
            else 
                resolve(data);
        });
    });
};

Though, you will quickly find that once you start using promises, you want to use them for all async operations so you'll be "promisifying" lots of things and having a library or at least a generic function that will do that for you will save lots of time.