2

I'm trying to use a function with callback in forEach loop.

I need to wait for execution to complete before moving on to next step.

Here's my code:

const arr = '6,7,7,8,8,5,3,5,1'

const id = arr.split(',');

const length = id.length;


id.forEach( (x, index) => {
  (function FnWithCallback () {
    setTimeout(() => { console.log(x) }, 5000);
  })();
});

console.log('done');

I came up with a hack:

const arr = '6,7,7,8,8,5,3,5,1'

const id = arr.split(',');

const length = id.length;

const fn = () => {
  return new Promise (resolve => {
    id.forEach( (id, index) => {
      setTimeout(() => {console.log(id)}, 3000);

      if(index === (length - 1))
         resolve();
    })
  })
}

fn().then(()=> {
  console.log('done');
})

But the hack seems to be broken.

Can I have a real solution to this? A NPM package would be really helpful.

Note: I had a look at async.js. I'm not sure if that is something I want since I'm trying to avoid callback hell.

Dev Aggarwal
  • 763
  • 1
  • 6
  • 19
  • Hi there! Apologize for my confusion, but what are you trying to accomplish? Do you just want to print out each index of `id`? Is `id` being modified by some other code that's not shown? I'm not seeing why this needs to be done asynchronously. – broAhmed Mar 09 '19 at 15:39
  • @broAhmed Actually, callback function is inserting values in DB. – Dev Aggarwal Mar 09 '19 at 15:41
  • @JonasWilms Wait in order. – Dev Aggarwal Mar 09 '19 at 15:41
  • 2
    The npm package you are looking for is [async.js](https://www.npmjs.com/package/async). Or just go for promises [and don't use `forEach`](https://stackoverflow.com/a/37576787/1048572) – Bergi Mar 09 '19 at 15:52

4 Answers4

5

The solution is to promisify the callback function and then use Array.prototype.map() coupled with Promise.all():

const arr = '6,7,7,8,8,5,3,5,1'

function FnWithCallback (id, cb) {
  setTimeout(cb, 1000, id)
}

const promisified = id => new Promise(resolve => {
  FnWithCallback(id, resolve)
})

const promises = arr.split(',').map(promisified)

Promise.all(promises).then(id => {
  console.log(id)
  console.log('Done')
})

If your callback API follows the Node.js convention of (error, result) => ..., then you should use util.promisify() to promisify the function, or check the documentation to see if omitting the callback argument will cause the call to return a promise, since a lot of packages provide promise-based APIs out-of-the-box now.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
4

If you want to make sure that the async actions aren't running in parallel but one after another, read on. If you want to get the results in order but don't care if they run in parallel, see Patrick's answer.

You could use an async function to be able to await promises in a for loop, for that a promisified timer is really useful:

const timer = ms => new Promise(res => setTimeout(res, ms));

(async function() {
  for(const id of ["6", "7", "7" /*...*/]) {
     await timer(5000);
     console.log(id);
  }
  console.log("done");
})();

This could also be achieved using a callback chain, however I'm not sure if that is understandable / useful (just wanted to show that callbacks doesnt have to be from hell):

["6", "7", "7" /*..*/].reduceRight(
  (next, id) => () => setTimeout(() => {
     console.log(id);
     next();
  }, 5000),
  () => console.log("done")
)();
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • 1
    This will cause warnings with some linters for `await` inside loop since it is decreasing the throughput by delaying each call until the previous finishes rather than starting them in parallel. – Patrick Roberts Mar 09 '19 at 15:47
  • 1
    @patrick I asked the OP and the response was "wait in order", which seems like that is wanted. – Jonas Wilms Mar 09 '19 at 15:48
  • 1
    They might be confused about the difference between "getting the results in order" and "making them chronologically dependent" – Patrick Roberts Mar 09 '19 at 15:49
1

You can chain promises using Array.prototype.reduce() starting with an initial resolved promise. You will have to turn your callback into a promise so you can chain them:

const arr = '6,7,7,8,8,5,3,5,1'
const ids = arr.split(',');
const length = ids.length;

const timeout = (fn, ms) => new Promise(res => {
  setTimeout(() => { fn(); res(); }, ms);
});

ids.reduce((previousPromise, id) => {
  return previousPromise.then(() => {
    return timeout(() => console.log(id), 200);
  });
}, Promise.resolve());

console.log('done');

Or using async/await:

const arr = '6,7,7,8,8,5,3,5,1'
const ids = arr.split(',');
const length = ids.length;

const timeout = (fn, ms) => new Promise(res => {
  setTimeout(() => { fn(); res(); }, ms);
});

ids.reduce(async (previousPromise, id) => {
  await previousPromise;
  return timeout(() => console.log(id), 200);
}, Promise.resolve());

console.log('done');
jo_va
  • 13,504
  • 3
  • 23
  • 47
0

You can try this approach, where promisefy will receive any type of value as the argument to be resolved after.

const IDs = '6,7,7,8,8,5,3,5,1'.split(',');

// simulates an async response after 1s
const promisefy = (value) => new Promise((resolve) => {
  setTimeout(() => {
    resolve(value);
  }, 1000);
});

// stores all async responses
const responses = IDs.map((id, index) => {
  return promisefy(id);
});

// executes sequentially all responses
responses.forEach((resp, index) => {
  resp.then((value) => console.log(`id: ${value}, index: ${index}`));
});

Or using Array reduce()

// stores all async responses
const responses2 = IDs.map((id) => promisefy(id));

// executes sequentially all responses
responses2
  .reduce((_, resp, index) => {
      return resp.then((value) => console.log(`id: ${value}, index: ${index}`));
    }, null)
  .then(() => console.log('done'));
jherax
  • 5,238
  • 5
  • 38
  • 50