Dev Aggarwal
Dev Aggarwal

Reputation: 783

function with callback in foreach loop

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.

Upvotes: 2

Views: 577

Answers (4)

jherax
jherax

Reputation: 5267

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'));

Upvotes: 0

jo_va
jo_va

Reputation: 13983

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');

Upvotes: 1

Jonas Wilms
Jonas Wilms

Reputation: 138537

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")
)();

Upvotes: 4

Patrick Roberts
Patrick Roberts

Reputation: 51957

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.

Upvotes: 5

Related Questions