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 await
ing 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.