-1

This article says below code does not woking since forEach isn't async-aware.

story.chapterUrls.forEach(function(chapterUrl) {
    getJSON(chapterUrl).then(function(chapter) {
        addHtmlToPage(chapter.html);
    });
})

story.chapterUrls is a array containing urls and getJSON fetches data from url. However above code seems to be working although each getJSON() blocks next one. Waht is the problem with that code?

soonoo
  • 867
  • 1
  • 10
  • 35
  • 3
    What do you mean it blocks the next one? According to the article you just cited, they don't. The problem is that because they don't block each other, `addHtmlToPage()` could be called in any order. – Patrick Roberts Jan 19 '18 at 20:49
  • @PatrickRoberts In `forEach`, execution order is like `getJSON(url[0])` -> `getJSON(url[1])` -> `getJSON(url[2])` ... and each one is non blocking since `getJSON()` returns promise. Is this right?? – soonoo Jan 19 '18 at 21:09
  • Right, non-blocking. But, it's certainly possible for the server-side logic that handles those requests to be blocking, thus forcing them to be in FIFO order. It's also possible that this is just coincidence and if you run it enough times, they'll eventually run out of order. – Kevin B Jan 19 '18 at 21:11
  • Thanks. I've tested the code with api that returns very small data so I thought each `getJSON` blocks next one. – soonoo Jan 19 '18 at 21:19
  • @KevinB If you want to "downvote", mark as duplicate, or otherwise address the question as-interpreted https://stackoverflow.com/questions/48349699/why-does-array-prototype-foreach-not-recognize-await-within-an-async-function – guest271314 Jan 19 '18 at 21:21

1 Answers1

1

TL;DR The optimal solution is at the bottom

The problem, as I explained earlier, is that while the execution order of getJSON() is guaranteed by forEach(), the execution order of the callback to each .then() is not guaranteed, since it is executed asynchronously.

Two canonical ways to solve this is to use Promise.all() or Array#reduce():

Promise.all():

Promise.all(story.chapterUrls.map(function(chapterUrl) {
  return getJSON(chapterUrl);
})).then(chapters => chapters.forEach(function(chapter) {
  addHtmlToPage(chapter.html);
});

With Promise.all(), the .then() callback waits for all chapters to resolve, then adds all the HTML at the same time. This has the disadvantage of leaving the page blank a bit longer than the methods below, but overall will finish faster because all the asynchronous requests are happening in parallel.

Array#reduce():

story.chapterUrls.reduce(async function(promise, chapterUrl) {
  await promise;
  const chapter = await getJSON(chapterUrl);
  addHtmlToPage(chapter.html);
}, Promise.resolve());

With Array#reduce(), a promise chain is created that loads each chapter asynchronously in sequential order, adding the HTML immediately after each one is fetched. This has the advantage of putting content on the page as soon as possible, but will take longer overall because it does not fetch resources in parallel.

Using a tricky little change to the above approach, you can load the resources in parallel and display them sequentially as they load by awaiting on the promise chain after fetching resources:

story.chapterUrls.reduce(async function(promise, chapterUrl) {
  const chapter = await getJSON(chapterUrl);
  await promise;
  addHtmlToPage(chapter.html);
}, Promise.resolve());

However, if you want to render all the content at once at the expense of keeping the page blank a little longer, go with the Promise.all() approach.

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