How to fetch() multiple resources? - javascript

I have a list of addresses pointing to json resources. I want to download those files to be able to use them in later processing.
I have this piece of code that uses the fetch() method:
let urlList = [
"https://url.to/resource1.json",
"https://url.to/resource2.json"
];
let promiseList = [];
let jsonBaseList = [];
urlList.forEach (function (url, i) {
promiseList.push (
fetch(url).then (function (res) {
jsonBaseList[i] = res.json();
})
);
});
Promise
.all(promiseList)
.then (function () {
console.log('All done.');
})
console.log('jsonBaseList: ', jsonBaseList)
Thus, the jsonBaseList contains a list of promises.
But I just want a list of json objects instead.
It's possible?

You should return that res.json() and use the resolved value in the next .then, since Response.json() returns another promise.
let urlList = [
"https://url.to/resource1.json",
"https://url.to/resource2.json"
];
let promiseList = [];
let jsonBaseList = [];
urlList.forEach (function (url, i) {
promiseList.push (
fetch(url).then (function (res) {
return res.json();
}).then(function (res) {
jsonBaseList[i] = res;
})
);
});
Promise
.all(promiseList)
.then (function () {
console.log('All done.');
})
console.log('jsonBaseList: ', jsonBaseList)
Update: I just edited your current code in order to make it work. But you can write it better:
let urlList = [
"https://url.to/resource1.json",
"https://url.to/resource2.json"
];
let jsonBaseList = [];
const promiseList = urlList.map((url) => {
return fetch(url)
.then(response => response.json())
})
Promise.all(promiseList).then(values => {
jsonBaseList = values;
console.log('All done.');
})
console.log('jsonBaseList: ', jsonBaseList)
Update: The console.log at the end of the code will output an empty array since promises are run asynchronously after the current script is run. So you can:
Put it inside .then
or put it in a chained .then
or use the async/await syntax (a cleaner way to write promises)
(async function() {
let urlList = [
"https://url.to/resource1.json",
"https://url.to/resource2.json"
];
const promiseList = urlList.map((url) => {
return fetch(url)
.then(response => response.json())
})
const jsonBaseList = await Promise.all(promiseList)
console.log('All done.');
console.log('jsonBaseList: ', jsonBaseList)
})()

Thus, the jsonBaseList contains a list of promises.
Yes, because res.json() returns a promise. But as of when you show your console.log, jsonBaseList will be [] because that code runs before any of the promises settle.
The minimal change to your code for jsonBaseList to have values in it in code run after the promises settle is:
// ...
// ...
let urlList = [
"https://url.to/resource1.json",
"https://url.to/resource2.json"
];
let promiseList = [];
let jsonBaseList = [];
urlList.forEach (function (url, i) {
promiseList.push (
fetch(url) .then (function (res) {
return res.json();
})
.then (function (value) { // ***
jsonBaseList[i] = value; // *** This is what I added
}) // ***
);
});
Promise
.all(promiseList)
.then (function () {
console.log('All done.'); // *** Use `jsonBaseList` here
})
// Removed the `console.log` here that would have logged `[]`
but it can be much simpler; all of the above can be replaced with:
let urlList = [
"https://url.to/resource1.json",
"https://url.to/resource2.json"
];
Promise.all(urlList.map(url => fetch(url).then(response => response.json())))
.then(jsonBaseList => {
// ...use `jsonBaseList` here...
})
.catch(error => {
// ...handle/report error here...
});
Notice that you use jsonBaseList in the then handler on Promise.all. I don't declare it in a broader scope because it's not filled in until that handler is called (that's the reason your console.log at the end will always log []). If you declare it in a broader scope, you make it likely you'll try to use it before it's available (as in the question's code).
But if want it in a broader scope and you realize it won't be filled in until later, add:
let jsonBaseList = []; // *** Not filled in until the promises settle!
and then change
.then(jsonBaseList => {
// ...use `jsonBaseList` here...
})
to
.then(list => {
jsonBaseList = list;
})
(Or use const and jsonBaseList.push(...list).)
Side note: You probably want to handle the possibility that the HTTP request failed (even though the network request succeeded — this is a footgun in the fetch API I write about here, it doesn't rject on HTTP failure, just network failure). So:
let urlList = [
"https://url.to/resource1.json",
"https://url.to/resource2.json"
];
Promise.all(
urlList.map(
url => fetch(url).then(response => {
if (response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
})
)
)
.then(jsonBaseList => {
// ...use `jsonBaseList` here...
})
.catch(error => {
// ...handle/report error here...
});

You need to wait for your promises to be done first
await Promise.all(promiseList)
console.log('jsonBaseList: ', jsonBaseList)

Related

Promise wont return valid value

I have this test I made just to check an API, but then i tryied to add an URL from a second fetch using as parameter a value obtained in the first fetch and then return a value to add in the first fecth. The idea is to add the image URL to the link. thanks in advance.
function script() {
const url = 'https://pokeapi.co/api/v2/pokemon/?offset=20&limit=20'
const result = fetch(url)
.then( (res)=>{
if(res.ok) {
return res.json()
} else {
console.log("Error!!")
}
}).then( data => {
console.log(data)
const main = document.getElementById('main');
main.innerHTML=`<p><a href='${data.next}'>Next</a></p>`;
for(let i=0; i<data.results.length;i++){
main.innerHTML=main.innerHTML+`<p><a href=${getImageURL(data.results[i].url)}>${data.results[i].name}</a></p>`;
}
})
}
async function getImageURL(imgUrl) {
const resultImg = await fetch(imgUrl)
.then( (res)=> {
return res.json()
})
.then (data => {
console.log(data.sprites.other.dream_world.front_default);
})
return resultImg.sprites.other.dream_world.front_default;
}
In general, don't mix .then/.catch handlers with async/await. There's usually no need, and it can trip you up like this.
The problem is that your fulfillment handler (the .then callback) doesn't return anything, so the promise it creates is fulfilled with undefined.
You could return data, but really just don't use .then/.catch at all:
async function getImageURL(imgUrl) {
const res = await fetch(imgUrl);
if (!res.ok) {
throw new Error(`HTTP error ${res.status}`);
}
const resultImg = await res.json();
return resultImg.sprites.other.dream_world.front_default;
}
[Note I added a check of res.ok. This is (IMHO) a footgun in the fetch API, it doesn't reject its promise on HTTP errors (like 404 or 500), only on network errors. You have to check explicitly for HTTP errors. (I wrote it up on my anemic old blog here.)]
There's also a problem where you use getImageURL:
// Incorrent
for (let i = 0; i < data.results.length; i++) {
main.innerHTML=main.innerHTML+`<p><a href=${getImageURL(data.results[i].url)}>${data.results[i].name}</a></p>`;
}
The problen here is that getImageURL, like all async functions, returns a promise. You're trying to use it as those it returned the fulfillment value you're expecting, but it can't — it doesn't have that value yet.
Instead, you need to wait for the promise(s) youre creating in that loop to be fulfilled. Since that loop is in synchronous code (not an async function), we'd go back to .then/.catch, and since we want to wait for a group of things to finish that can be done in parallel, we'd do that with Promise.all:
// ...
const main = document.getElementById('main');
const html = `<p><a href='${data.next}'>Next</a></p>`;
Promise.all(data.results.map(async ({url, name}) => {
const realUrl = await getImageURL(url);
return `<p><a href=${realUrl}>${name}</a></p>`;
}))
.then(paragraphs => {
html += paragraphs.join("");
main.innerHTML = html;
})
.catch(error => {
// ...handle/report error...
});
For one, your
.then (data => {
console.log(//...
at the end of the promise chain returns undefined. Just remove it, and if you want to console.log it, do console.log(resultImg) in the next statement/next line, after await.
This the final version that accomplish my goal. Just want to leave this just in case someone finds it usefull. Thanks for those who answer!
function script() {
const url = 'https://pokeapi.co/api/v2/pokemon/?offset=20&limit=20'
const result = fetch(url)
.then( (res)=>{
if(res.ok) {
return res.json()
} else {
console.log("Error!!")
}
}).then( data => {
console.log(data)
const main = document.getElementById('main');
main.innerHTML=`<p><a href='${data.next}'>Proxima Página</a></p>`;
Promise.all(data.results.map(async ({url, name}) => {
const realUrl = await getImageURL(url);
return `<div><a href=${realUrl}>${name}</a></div>`;
}))
.then(paragraphs => {
main.innerHTML=main.innerHTML+paragraphs;
})
.catch(error => {
console.log(error);
});
})
}
async function getImageURL(imgUrl) {
const res = await fetch(imgUrl);
if(!res.ok) {
throw new Error(`HTTP Error ${res.status}`)
}
const resultImg = await res.json();
return resultImg.sprites.other.dream_world.front_default
}

How to have a function finish a for loop before returning

Below is the code I type to go through an array and stick the data needed into an output array, then ultimately return the output array:
var stocks = ['AAPL', 'BMY']
let getInfo = arr => {
let output = []
arr.forEach(i => {
var root = 'https://fmpcloud.io/api/v3/profile/' + i + '?apikey=myAPIKey'
axios.get(root)
.then((data) => {
output.push(data.data)
})
.catch((e) => {
console.log('error', e)
})
})
console.log('output: ', output)
return output
}
getInfo(stocks)
The console.log just logs an empty array, which makes me think it goes to the return statement before the for loop finishes executing. Does anybody know the best way to have the for loop finish first, and then finally return the output array?
You can use async/await keywords to get the axios call to wait. Like:
let getInfo = async arr => {
let output = []
try {
for(var i in arr) {
var root = 'https://fmpcloud.io/api/v3/profile/' + arr[i] + '?apikey=myAPIKey'
var data = await axios.get(root);
output.push(data.data);
}
}
catch(e) {
// oops
console.log('error', e);
return null;
}
return output;
}
That's because axios is the asynchronous method and return statement should be returned before the axios().then().
Use Promise.all() and use async/await
var stocks = ['AAPL', 'BMY']
let getInfo = async arr => {
let output = []
const promises = []
arr.forEach(i => {
var root = 'https://fmpcloud.io/api/v3/profile/' + i + '?apikey=myAPIKey'
promises.push(axios.get(root))
.then((data) => {
output.push(data.data)
})
.catch((e) => {
console.log('error', e)
})
})
const result = await Promise.all(promises)
output = result.map(r => r.data)
console.log('output: ', output)
return output
}
getInfo(stocks)
Axios.get returns a promise, it is executed asynchronously while the rest of your lambda continues its execution.
Look into Promise.all() so that you can log output after all the promises have resolved.
var stocks = ["AAPL", "BMY"];
let getInfo = async (arr) => {
const output = await Promise.all(
arr.map(
(i) =>
new Promise(async (resolve, reject) => {
try {
const { data } = await axios.get(
`https://fmpcloud.io/api/v3/profile/${i}?apikey=myAPIKey`
);
resolve(data);
} catch (err) {
reject(err);
}
})
)
);
console.log("output: ", output);
return output;
};
getInfo(stocks);
Don't use forEach. Use a for/of loop. Next, use async/await to synchronize on the
axios calls. That should do it.

How do I make a long list of http calls in serial?

I'm trying to only make one http call at time but when I log the response from getUrl they are piling up and I start to get 409s (Too many requests)
function getUrl(url, i, cb) {
const fetchUrl = `https://api.scraperapi.com?api_key=xxx&url=${url.url}`;
fetch(fetchUrl).then(async res => {
console.log(fetchUrl, 'fetched!');
if (!res.ok) {
const err = await res.text();
throw err.message || res.statusText;
}
url.data = await res.text();
cb(url);
});
}
let requests = urls.map((url, i) => {
return new Promise(resolve => {
getUrl(url, i, resolve);
});
});
const all = await requests.reduce((promiseChain, currentTask) => {
return promiseChain.then(chainResults =>
currentTask.then(currentResult => [...chainResults, currentResult]),
);
}, Promise.resolve([]));
Basically I don't want the next http to start until the previous one has finished. Otherwise I hammer their server.
BONUS POINTS: Make this work with 5 at a time in parallel.
Since you're using await, it would be a lot easier to use that everywhere instead of using confusing .thens with reduce. It'd also be good to avoid the explicit Promise construction antipattern. This should do what you want:
const results = [];
for (const url of urls) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(response); // or whatever logic you need with errors
}
results.push(await response.text());
}
Then your results variable will contain an array of response texts (or an error will have been thrown, and the code won't reach the bottom).
The syntax for an async function is an async keyword before the argument list, just like you're doing in your original code:
const fn = async () => {
const results = [];
for (const url of urls) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(response); // or whatever logic you need with errors
}
results.push(await response.text());
}
// do something with results
};
To have a limited number of requests at a time, make a queue system - when a request completes, recursively call a function that makes another request, something like:
const results = [];
const queueNext = async () => {
if (!urls.length) return;
const url = urls.shift();
const response = await fetch(url);
if (!response.ok) {
throw new Error(response); // or whatever logic you need with errors
}
results.push(await response.text());
await queueNext();
}
await Promise.all(Array.from({ length: 5 }, queueNext));
// do something with results
You cannot use Array methods to sequentually run async operations because array methods are all synchronous.
The easiest way to achieve sequential async tasks is through a loop. Otherwise, you will need to write a custom function to imitate a loop and run .then after a async task ends, which is quite troublesome and unnecessary.
Also, fetch is already returning a Promise, so you don't have to create a Promise yourself to contain that promise returned by fetch.
The code below is a working example, with small changes to your original code (see comments).
// Fake urls for example purpose
const urls = [{ url: 'abc' }, { url: 'def', }, { url: 'ghi' }];
// To imitate actual fetching
const fetch = (url) => new Promise(resolve => {
setTimeout(() => {
resolve({
ok: true,
text: () => new Promise(res => setTimeout(() => res(url), 500))
});
}, 1000);
});
function getUrl(url, i, cb) {
const fetchUrl = `https://api.scraperapi.com?api_key=xxx&url=${url.url}`;
return fetch(fetchUrl).then(async res => { // <-- changes here
console.log(fetchUrl, 'fetched!');
if (!res.ok) {
const err = await res.text();
throw err.message || res.statusText;
}
url.data = await res.text();
return url; // <--- changes here
});
}
async function getAllUrls(urls){
const result = [];
for (const url of urls){
const response = await getUrl(url);
result.push(response);
}
return result;
}
getAllUrls(urls)
.then(console.log);
async/await is perfect for this.
Assuming you have an array of URLs as strings:
let urls = ["https://example.org/", "https://google.com/", "https://stackoverflow.com/"];
You simply need to do:
for (let u of urls) {
await fetch(u).then(res => {
// Handle response
}).catch(e => {
// Handle error
});
}
The loop will not iterate until the current fetch() has resolved, which will serialise things.
The reason array.map doesn't work is as follows:
async function doFetch(url) {
return await fetch(url).then(res => {
// Handle response
}).catch(e => {
// Handle error
});
}
let mapped = urls.map(doFetch);
is equivalent to:
let mapped;
for (u of urls) {
mapped.push(doFetch(u));
}
This will populate mapped with a bunch of Promises immediately, which is not what you want. The following is what you want:
let mapped;
for (u of urls) {
mapped.push(await doFetch(u));
}
But this is not what array.map() does. Therefore using an explicit for loop is necessary.
Many people provided answers using for loop. But in some situation await in for loop is not welcome, for example, if you are using Airbnb style guide.
Here is a solution using recursion.
// Fake urls for example purpose
const urls = [{ url: 'abc' }, { url: 'def', }, { url: 'ghi' }];
async function serialFetch(urls) {
return await doSerialRecursion(
async (url) => {
return result = await fetch(url)
.then((response) => {
// handle response
})
.catch((err) => {
// handle error
});
},
urls,
0
);
}
async function doSerialRecursion(fn, array, startIndex) {
if (!array[startIndex]) return [];
const currResult = await fn(array[startIndex]);
return [currResult, ...(await doSerialRecursion(array, fn, startIndex + 1))];
}
const yourResult = await serialFetch(urls);
The doSerialRecursion function will serially execute the function you passed in, which is fetch(url) in this example.

React Native Wait All Image Prefetch Done

I'm trying to prefetch multiple image before navigating to another screen, but returnedStudents all undefined.
prepareStudentImages = async (students) => {
let returnedStudents = students.map(student => {
Image.prefetch(student.image)
.then((data) => {
...
})
.catch((data) => {
...
})
.finally(() => {
return student;
});
});
await console.log(returnedStudents); // ----> all items undefined
}
There are a couple of things to fix with this:
1) Your map() function does not return anything. This is why your console log is undefined.
2) Once your map functions work, you are logging an array of promises. To deal with multiple promises (an array), you can use Promise.all().
So I think to fix this, you can do:
prepareStudentImages = async (students) => {
const returnedStudents = students.map(student =>
Image.prefetch(student.image)
.then((data) => {
...
})
.catch((data) => {
...
})
.finally(() => {
return student
})
)
console.log(returnedStudents) // log the promise array
const result = await Promise.all(returnedStudents) // wait until all asyncs are complete
console.log(result) // log the results of each promise in an array
return result
}

The best way to wait for the completion async function before returning data

What is the best way to wait for the completion of all parallel async functions before returning data?
Request works asynchronously and the following function will return an empty array.
import request from 'request'
// List of urls
const urls = [
'http://someurl.com/1.json',
'http://someurl.com/2.json',
'http://someurl.com/3.json',
]
function getData () {
// An array that will contain data
let result = []
// Request data from all urls
urls.forEach(i => {
// Function works asynchronously
request(i, (err, res, body) => {
if(err) throw err
const data = JSON.parse(body)
result.push(i.someValue)
})
})
return result // Returns an empty array :(
}
If you can use promises, the best way would be to use them.
Make sure your request function returns a promise, so you can then something like:
var request = function request( url ) {
return new Promise(function requestPromise( resolve, reject ) {
myAjaxCallOrOtherAsyncCall( url, function( error, response ) {
if (error) reject(error);
else resolve(response);
})
});
};
var getData = function getData( urls ) {
return Promise.all( urls.map(request) );
};
var urls = [
'http://someurl.com/1.json',
'http://someurl.com/2.json',
'http://someurl.com/3.json',
];
getData(urls).then(function( responses ) {
var results = responses.map(JSON.parse);
// do somethign with async results
});
Use Promise.all() to await all promises to complete, though this does require that you use a request library that returns promises, like axios.
import axios from 'axios'
// List of urls
const urls = [
'http://someurl.com/1.json',
'http://someurl.com/2.json',
'http://someurl.com/3.json',
]
function getData() {
return new Promise((resolve, reject) => {
const promises = urls.map(url => axios.get(url)); // array of pending promises
Promise.all(promises) // creates single promise that resolves when all `promises` resolve
.then(responses => {
const dataArray = responses.map(response => response.data);
return resolve(dataArray);
}) // resolves with an array of response data
.catch(reject);
})
}
getData()
.then(data => {
// do something
})
Try to use the trigger. When all data is collected use
$(trigger_obj).trigger('loading_complete');
And then make it a handler
$(trigger_obj).on('loading_complete', function () {
\\ logic ...
});
Using https://github.com/request/request-promise-native, you can facilitate Promise.all as others have already pointed out:
import Request from 'request-promise-native'
// List of urls
const urls = [
'http://someurl.com/1.json',
'http://someurl.com/2.json',
'http://someurl.com/3.json',
]
// The simplest form to create a bunch of request promises:
Promise.all(urls.map(Request))
// Otherwise it could look as follows:
const urlPromises = urls.map(url => Request({
uri: 'http://www.google.com'
// and more options
}))
// Or with promise chains:
const urlPromiseChain = (url) => {
return Request(url)
.then(doSomethingWithResponse)
.catch(logButDontFailOnError)
}
const urlPromiseChains = urls.map(urlPromiseChain)
Promise.all(urlPromises /* or urlPromiseChains */)
.then(responses => console.log(responses))

Categories