88

If I have an array of urls:

var urls = ['1.txt', '2.txt', '3.txt']; // these text files contain "one", "two", "three", respectively.

And I want to build an object that looks like this:

var text = ['one', 'two', 'three'];

I’ve been trying to learn to do this with fetch, which of course returns Promises.

Some things I’ve tried that don’t work:

var promises = urls.map(url => fetch(url));
var texts = [];
Promise.all(promises)
  .then(results => {
     results.forEach(result => result.text()).then(t => texts.push(t))
  })

This doesn’t look right, and in any case it doesn’t work — I don’t end up with an array ['one', 'two', 'three'].

Is using Promise.all the right approach here?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • That looks like a bracketing mistake. Did you really intend to call `.then` on the return value of `.forEach(…)`, or rather on `….text()`? – Bergi Jul 29 '15 at 21:35
  • Where do you look at / log `texts` and observe it to be still empty? – Bergi Jul 29 '15 at 21:36

7 Answers7

161

Yes, Promise.all is the right approach, but you actually need it twice if you want to first fetch all urls and then get all texts from them (which again are promises for the body of the response). So you'd need to do

Promise.all(urls.map(u=>fetch(u))).then(responses =>
    Promise.all(responses.map(res => res.text()))
).then(texts => {
    …
})

Your current code is not working because forEach returns nothing (neither an array nor a promise).

Of course you can simplify that and start with getting the body from each response right after the respective fetch promise fulfilled:

Promise.all(urls.map(url =>
    fetch(url).then(resp => resp.text())
)).then(texts => {
    …
})

or the same thing with await:

const texts = await Promise.all(urls.map(async url => {
  const resp = await fetch(url);
  return resp.text();
}));
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 3
    Addressing something I sensed in the question: You can't "extract" results to an outside variable because of how async works in JavaScript, but you can use generators or async/await to simulate it. See [this answer](http://stackoverflow.com/a/30180679/1348195) for a complete guide on asynchronisity in JS. – Benjamin Gruenbaum Jul 29 '15 at 21:49
  • this looks amazing ! but I can't wrap my head around :( javascript is a strange language – yota Aug 23 '17 at 07:14
  • @sansSpoon Are you sure you used the concise arrow body that I have in my answer? It *does* implicitly return the promise. – Bergi Apr 24 '18 at 10:38
  • Doh, of course, palm > face. I'm always mixing my es5/6. Thanks. – sansSpoon Apr 24 '18 at 11:07
  • Why do you need a second `Promise.all`? [MDN states](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) that _"The Promise.all() method returns a single Promise that resolves when all of the promises passed as an iterable have resolved or when the iterable contains no promises."_ which makes me think there should return a single promise, not an array of them that will need the second `Promise.all` But it won't work without it – 1252748 May 10 '19 at 22:18
  • 1
    @1252748 Yes, the `Promise.all()` returns a single promise for an array or response objects. But then, `responses.map(res => res.text())` produces another array [of promises](https://stackoverflow.com/a/37555432/1048572) which needs a second `Promise.all`. – Bergi May 11 '19 at 09:09
31

For some reason neither of Bergi's examples worked for me. It would simply give me empty results. After some debugging it seemes like the promise would return before the fetch had finished, hence the empty results.

However, Benjamin Gruenbaum had an answer here earlier, but deleted it. His method did work for me, so I'll just copy-paste it here, as an alternative in case anyone else runs into any problems with the first solution here.

var promises = urls.map(url => fetch(url).then(y => y.text()));
Promise.all(promises).then(results => {
    // do something with results.
});
peirix
  • 36,512
  • 23
  • 96
  • 126
  • Thank you, I've looked at several SO answers pertaining to doing something only after the promised task(s) have actually finished working. This answer actually waited for all files to finish fetching before triggering my callback. – Reahreic Apr 01 '21 at 13:06
  • There is really no difference between your (/ Benjamin Gruenbaum's) answer and the second snippet by Bergi. — Suppose I replace your second `promises` with `urls.map(url => fetch(url).then(y => y.text()))` and then replace your `y` with `resp`, an finally replace your `results` with `texts`. The only remainig difference to Bergi's second snippet is the comment `// do something with results`, and where the lines break (well — and the final semicolon). (Might this explain why Benjamin Gruenbaum removed his answer?) – Henke May 20 '21 at 09:45
14

You should use map instead of forEach:

Promise.all(urls.map(url => fetch(url)))
.then(resp => Promise.all( resp.map(r => r.text()) ))
.then(result => {
    // ...
});
evgenAborigen
  • 175
  • 1
  • 7
5

Here is a clean way to do it.

const requests = urls.map((url) => fetch(url)); 
const responses = await Promise.all(requests); 
const promises = responses.map((response) => response.text());
return await Promise.all(promises);
Derek
  • 5,137
  • 9
  • 28
  • 39
4

The suggested array urls = ['1.txt', '2.txt', '3.txt'] does not make much sense to me, so I will instead use:

urls = ['https://jsonplaceholder.typicode.com/todos/2',
        'https://jsonplaceholder.typicode.com/todos/3']

The JSONs of the two URLs:

{"userId":1,"id":2,"title":"quis ut nam facilis et officia qui",
 "completed":false}
{"userId":1,"id":3,"title":"fugiat veniam minus","completed":false}

The goal is to get an array of objects, where each object contains the title value from the corresponding URL.

To make it a little more interesting, I will assume that there is already an array of names that I want the array of URL results (the titles) to be merged with:

namesonly = ['two', 'three']

The desired output is an array of objects:

[{"name":"two","loremipsum":"quis ut nam facilis et officia qui"},
{"name":"three","loremipsum":"fugiat veniam minus"}]

where I have changed the attribute name title to loremipsum.

const namesonly = ['two', 'three'];
const urls = ['https://jsonplaceholder.typicode.com/todos/2',
  'https://jsonplaceholder.typicode.com/todos/3'];

Promise.all(urls.map(url => fetch(url)
  .then(response => response.json())
  .then(responseBody => responseBody.title)))
  .then(titles => {
    const names = namesonly.map(value => ({ name: value }));
    console.log('names: ' + JSON.stringify(names));
    const fakeLatins = titles.map(value => ({ loremipsum: value }));
    console.log('fakeLatins:\n' + JSON.stringify(fakeLatins));
    const result =
      names.map((item, i) => Object.assign({}, item, fakeLatins[i]));
    console.log('result:\n' + JSON.stringify(result));
  })
  .catch(err => {
    console.error('Failed to fetch one or more of these URLs:');
    console.log(urls);
    console.error(err);
  });
.as-console-wrapper { max-height: 100% !important; top: 0; }

Reference

Henke
  • 4,445
  • 3
  • 31
  • 44
2

In case, if you are using axios. We can achieve this like:

const apiCall = (endpoint:string)=> axios.get(${baseUrl}/${endpoint})

axios.all([apiCall('https://first-endpoint'),apiCall('https://second-endpoint')]).then(response => {
            response.forEach(values => values)
            }).catch(error => {})  
Usman Raza
  • 121
  • 1
  • 4
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Sep 30 '21 at 11:39
2

The following also works for me.

    Promise.all([
      fetch(QUESTIONS_API_BASE_URL).then(res => res.json()),
      fetch(SUBMISSIONS_API_BASE_URL).then(res => res.json())
    ])
    .then(console.log)
windmaomao
  • 7,120
  • 2
  • 32
  • 36