Paweł
Paweł

Reputation: 4516

waiting for many async functions execution

I have the promise function that execute async function in the loop few times for different data. I want to wait till all async functions will be executed and then resolve(), (or call callback function in non-promise function):

var readFiles = ()=>{
  return new Promise((resolve,reject)=>{
    var iterator = 0;
    var contents = {};
    for(let i in this.files){
      iterator++;
      let p = path.resolve(this.componentPath,this.files[i]);
      fs.readFile(p,{encoding:'utf8'},(err,data)=>{
        if(err){
          reject(`Could not read ${this.files[i]} file.`);
        } else {
          contents[this.files[i]] = data;
          iterator--;
          if(!iterator) resolve(contents);
        }
      });
    }
    if(!iterator) resolve(contents); //in case of !this.files.length
  });
};

I increase iterator on every loop repetition, then, in async function's callback decrease iterator and check if all async functions are done (iterator===0), if so - call resolve().

It works great, but seems not elegant and readable. Do you know any better way for this issue?

Upvotes: 0

Views: 480

Answers (3)

Amadan
Amadan

Reputation: 198304

Copied from comments:

Also - you might want to use fs-extra, a drop-in replacement for fs, but with promise support added.

Here's how that goes:

const fs = require('fs-extra');
var readFiles = ()=>{
  let promises = files
    .map(file => path.resolve(componentPath, file))
    .map(path => fs.readFile(path));
  return Promise.all(promises);
});

Nice and clean. You can then get contents like this:

readFiles()
.then(contents => { ... })
.catch(error => { ... });

This will fail on first error though (because that's what Promise.all does). If you want individual error handling, you can add another map line:

.map(promise => promise.catch(err => err));

Then you can filter the results:

let errors = contents.filter(content => content instanceof Error)
let successes = contents.filter(content => !(content instanceof Error))

Upvotes: 1

Juorder Gonzalez
Juorder Gonzalez

Reputation: 1652

You only need push all the Promises into an array to then pass it as argument to Promise.all(arrayOfPromises)

try something like this:

var readFiles = () => {
  var promises = [];
  let contents = {};
  var keys_files = Object.keys(this.files);
  if (keys_files.length <= 0) {
   var promise = new Promise((resolve, reject) => {
     resolve(contents);
   });
   promises.push(promise);
  }

  keys_files.forEach((key) => {
    var file = this.files[key];
    var promise = new Promise((resolve, reject) => {
       const currentPath = path.resolve(this.componentPath, file);
       fs.readFile(p,{encoding:'utf8'},(err, data) => {
         if (err) {
            return reject(`Could not read ${file} file.`);
         }

         contents[file] = data;
         resolve(contents)
       });
    });
  });

  return Promises.all(promises);
}

Then you should use the function like so:

// this will return a promise that contains an array of promises
var readAllFiles = readFiles();
// the then block only will execute if all promises were resolved if one of them were reject so all the process was rejected automatically
readAllFiles.then((promises) => {
   promises.forEach((respond) => {
      console.log(respond);
   });
}).catch((error) => error);

If you don't care if one of the promises was rejected, maybe you should do the following

var readFiles = () => {
  var promises = [];
  let contents = {};
  var keys_files = Object.keys(this.files);
  if (keys_files.length <= 0) {
   var promise = new Promise((resolve, reject) => {
     resolve(contents);
   });
   promises.push(promise);
  }

  keys_files.forEach((key) => {
    var file = this.files[key];
    var promise = new Promise((resolve, reject) => {
       const currentPath = path.resolve(this.componentPath, file);
       fs.readFile(p,{encoding:'utf8'},(err, data) => {
         // create an object with the information
         let info = { completed: true };
         if (err) {
            info.completed = false;
            info.error = err;
            return resolve(info);
         }

         info.data = data;
         contents[file] = info;
         resolve(contents)
       });
    });
  });

  return Promises.all(promises);
}

Upvotes: 1

dpaulus
dpaulus

Reputation: 422

Following up the comment with some code and more detail!

Promise.all() takes an iterator, and waits for all promises to either resolve or reject. It will then return the results of all the promises. So instead of keeping track of when all promises resolve, we can create little promises and add them to an array. Then, use Promise.all() to wait for all of them to resolve.

const readFiles = () => {
  const promises = [];

  for(let i in files) {
    const p = path.resolve(componentPath, files[i]);

    promises.push(new Promise((resolve, reject) => {
      fs.readFile(p, {encoding:'utf8'}, (err, data) => {
        if(err) {
          reject(`Could not read ${files[i]} file.`);
        } else {
          resolve(data);
        }
      });
    }));
  }

  return Promise.all(promises);
};

const fileContents = readFiles().then(contents => {
    console.log(contents)
})
.catch(err => console.error(err));

Upvotes: 2

Related Questions