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.

4
  • 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. Commented Mar 9, 2019 at 15:39
  • @broAhmed Actually, callback function is inserting values in DB. Commented Mar 9, 2019 at 15:41
  • @JonasWilms Wait in order. Commented Mar 9, 2019 at 15:41
  • 2
    The npm package you are looking for is async.js. Or just go for promises and don't use forEach Commented Mar 9, 2019 at 15:52

4 Answers 4

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.

Sign up to request clarification or add additional context in comments.

Comments

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

3 Comments

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 I asked the OP and the response was "wait in order", which seems like that is wanted.
They might be confused about the difference between "getting the results in order" and "making them chronologically dependent"
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');

Comments

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.