3

In the code below, I expect mark-1 to fire before mark-2. I'm guessing that I'm not using await and async correctly some where.

I'm pretty sure that this line:

  const things = await fs.promises.readdir(folder);

and this line

  const stats = await fs.promises.stat(path);

are correct as we are awaiting the file system to respond.

I'm not conerned with error checking or cross-platform code as of yet, just getting the promises to work correctly

// Libraries
const fs = require('fs');

// API
getThings('_t/').then(() => {
  console.log('mark-2')
})

// Accesses the file system and returns an array of files / folders
async function getThings (folder) {
  const things = await fs.promises.readdir(folder);
  things.forEach(async (thing)=>{
    await getStats(thing, folder);
  });
}

// Gets statistics for each file/folder
async function getStats (thing, folder) {
  const path = folder + thing;
  const stats = await fs.promises.stat(path);
  console.log('mark-1');
}

1 Answers1

3

The problem is that you are using async and await in the forEach call, and this doesn't work as you would expect.

The forEach method doesn't really care about the return value of the callback function (in this case the promise that getStats returns).

You should map the things array to promises, and use Promise.all:

async function getThings (folder) {
  const things = await fs.promises.readdir(folder);
  const promises = things.map(thing =>  getStats(thing, folder));
  return await Promise.all(promises);
}

Note that this will execute the promises "in parallel", and not sequentially.

If you want to execute the promises secuentially, one by one, you can either, "reduce" the promise array, or use a conventional loop (for, for-of).

EDIT:

Let me try to clarify why using an async callback function with forEach doesn't work.

Attempt #1: De-sugarizing it:

In the original example we have something like this:

// ...
things.forEach(async (thing)=>{
  await getStats(thing, folder);
});
// ...

If we separate the callback from the forEach, we have:

const callback = async (thing)=>{
    await getStats(thing, folder);
};

things.forEach(callback);

If we "desugarize" the async function:

const callback = function (thing) {
  return new Promise((resolve, reject) => {
    try {
      getStats(thing, folder).then(stat => /*do nothing*/);
    } catch (err) {
      reject(err);
    }
    resolve();
  });
};

things.forEach(callback);

Marking a function with async ensures the function will return always return a promise, regardless its completion, if the function reaches its execution without a explicit return value, the promise will be resolved with undefined, if it returns some value, the promise will resolve to it, and finally if something within the function throws, the promise will be rejected.

As you can see the problem is that the promises are just not being awaited by anything, and also they are not resolving to any value. The await placed in the callback does actually nothing with the value, just as in the above I'm doing .then and doing nothing with the value.

Attempt 2: Implementing a simple forEach function:

function forEach(array, callback) {
  for(const value of array) {
    callback(value);
  }
}

const values = [ 'a', 'b', 'c' ];
forEach(values, (item) => {
  console.log(item)
});

The above forEach is an oversimplification of the Array.prototype.forEach method, just to show its structure, in reality the callback function is called passing the array as the this value, and passing three arguments, the current element, the current index, and again the array instance, but we get the idea.

If we would like to implement an async forEach function, we would have to await the callback call:

const sleep = (time, value) => new Promise(resolve => setTimeout(resolve(value), time));
const values = [ { time: 300, value: 'a'}, { time: 200, value: 'b' }, {time: 100, value: 'c' } ];

async function forEachAsync(array, callback) {
  for(const value of array) {
    await callback(value);
  }
}

(async () => {

  await forEachAsync(values, async (item) => {
    console.log(await sleep(item.time, item.value))
  });
  
  console.log('done');
  
})()

The above forEachAsync function will iterate and await item by item, sequentially, normally you don't want that, if the async functions are independent, they can be done in parallel, just as I suggested in first place.

const sleep = (time, value) => new Promise(resolve => setTimeout(resolve(value), time));
const values = [ { time: 300, value: 'a'}, { time: 200, value: 'b' }, {time: 100, value: 'c' } ];

(async () => {

  const promises = values.map(item  => sleep(item.time, item.value));
  const result = await Promise.all(promises);
  console.log(result);
})()

And as you can see, even if the promises are executed in parallel, we get the results in the same order as the promises where in the array.

But the difference between this example and the first one is that this one takes only 300ms (the longest promise to resolve), and the first one takes 600ms (300ms + 200ms + 100ms).

Hope it makes it clearer.

Community
  • 1
  • 1
Christian C. Salvadó
  • 807,428
  • 183
  • 922
  • 838
  • Are you saying that await inside a forEach does not halt the program at that point? –  May 16 '19 at 23:38
  • @J.M., right, the forEach method will just fire the callback, the returned promises are simply lost. – Christian C. Salvadó May 16 '19 at 23:43
  • That goes against MDN. **await** is supposed to cause a wait regardless ... https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await –  May 16 '19 at 23:44
  • ... from MDN docs ... "The await expression causes async function execution to pause until a Promise is resolved" ... –  May 16 '19 at 23:45
  • No, it doesn't go against it. Just focus for a moment in the callback passed to forEach. The forEach method internally just executes the function, it doesn't await it, it doesn't actually care about the return value of the callback at all. – Christian C. Salvadó May 16 '19 at 23:48
  • I will come with an example to clarify it when I get back home – Christian C. Salvadó May 16 '19 at 23:51
  • @J.M., you're welcome, I have added some examples to help anybody who encounters this problem in the future, give it a look and tell me what you think. Cheers! – Christian C. Salvadó May 17 '19 at 02:52