I have an array of data objects about people. Each person object includes 0-n URLs for additional info (guests of the person).
I want to process this list, calling each of the 'guest' URLs and including the guest's names in the original data set.
Context: this is an AWS lambda function. I'm using lambda-local to run this locally. (lambda-local -l index.js -e fixtures/test_event1.json).
I'm successfully using await/async to retrieve the initial set of data.
But I'm unable to get if working for these further calls about guest info. It always shows a pending Promise, even though the result is awaited.
// index.js
const fetch = require('node-fetch');
exports.handler = async function(event){
try {
let registrations = await getEventRegistrations();
console.log(registrations);
/* All good here - sample console output
[ { contactId: 43452967,
displayName: 'aaa, bbb',
numGuests: 0,
guestUrls: [] },
{ contactId: 43766365,
displayName: 'bbb, ccc',
numGuests: 1,
guestUrls:
[ 'https://<URL>' ] },
{ contactId: 43766359,
displayName: 'ccc, ddd',
numGuests: 2,
guestUrls:
[ 'https://<URL>',
'https://<URL> ] } ]
*/
// Expanding the guest URLs is proving problematic - see expandGuests function below
registrations = registrations.map(expandGuests);
console.log(registrations);
/* Registrations are just pending Promises here, not the real data
[ Promise { <pending> },
Promise { <pending> },
Promise { <pending> } ]
*/
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(registrations),
};
}
catch (exception) {
console.log(exception);
return {
statusCode: 500,
body: 'Unable to retrieve data.'
};
}
};
function getEventRegistrations() {
return fetch('<URL>')
.then(res => res.json())
.catch(function (error) {
console.log('Event registrants request failed', error);
return null;
});
}
function getGuestName(url) {
return fetch(url)
.then(res => res.json())
.then(guest => guest.DisplayName)
.catch(function (error) {
console.log('Guest name request failed', error);
return null;
});
}
async function expandGuests(data) {
const promises = data.guestUrls.map(url => getGuestName(url));
data.guestNames = await Promise.all(promises);
return data;
}
How can I resolve these pending Promises and thus return useful data?
Thank you.
array.map doesn't contain any logic to wait for promises. It just calls the function for each element of the array, and then synchronously produces an array of the results. If you pass in async functions, then the result be an array of promises (since all async functions return promises).
You'll need to use Promise.all to create a promise which will resolve once all the individual promises have resolved, and then await that.
const promises = data.guestUrls.map(url => getGuestName(url));
data.guestNames = await Promise.all(promises);
P.S, i strongly recommend against mixing async/await with .then. The cases where you actually need to do so are very rare, so most of the time you just make the code harder to understand. For example, this function:
async function getGuestName(url) {
return await fetch(url)
.then(res => res.json())
.then(guest => guest.DisplayName)
.catch(function (error) {
console.log('Guest name request failed', error);
return null;
});
}
Should either be done like this:
async function getGuestName(url) {
try {
const res = await fetch(url);
const guest = await res.json();
return guest.DisplayName;
} catch (error) {
console.log('Guest name request failed', error);
return null;
}
}
Or like this
function getGuestName(url) {
return fetch(url)
.then(res => res.json())
.then(guest => guest.DisplayName)
.catch(function (error) {
console.log('Guest name request failed', error);
return null;
});
}
The comments are correct in pointing out that mapping an async function will return an array of Promises. What they don't explicitly mention is that you have two maps, and which one is problematic.
The issue is in the line:
reservations = reservations.map(expandGuests);
Any time you use map, you'll need to actually resolve the promises that return.
So:
const mappedPromises = reservations.map(expandGuests); //returns an Array of Pending promises
const mappedReservations = await Promise.all(mappedPromises); //resolves the promises
Related
On the way of learning the concepts of asynchronous JavaScript, I got struggled with the idea behind the situation when they can be chained. As an example consider the following situation: a webhook calls a cloud function and as a requirement there is set a time interval by which the cloud function should response to the webhook. In the cloud function is called an operation for fetching some data from a database, which can be short- or long-running task. For this reason we may want to return a promise to the webhook just to "register" a future activity and later provide results from it e.g.:
async function main () {
return new Promise ((resolve, reject) => {
try {
db.getSomeData().then(data=> {
resolve({ result: data});
});
} catch (err) {
reject({ error: err.message });
}
});
}
Another way is to use async/await instead of a promise for fetching the data, e.g.:
async function main () {
return new Promise (async (resolve, reject) => {
try {
const data = await db.getSomeData();
resolve({ result: data });
} catch (err) {
reject({ error: err.message });
}
});
}
(The code is a pseudo-code and therefore all checks on the returned types are not considered as important.)
Does the await block the code in the second example and prevent returning of the promise?
async functions always return a Promise. It is up to the function caller to determine how to handle it: either by chaining with then/catch or using await. In your example, there is no need to create and return a new Promise.
You could do something like this for example:
async function main () {
try {
const data = await db.getSomeData()
return {result: data}
} catch (err) {
return {error: err.message}
}
}
// On some other part of the code, you could handle this function
// in one of the following ways:
//
// 1.
// Chaining the Promise with `then/catch`.
main()
.then((result) => console.log(result)) // {result: data}
.catch((err) => console.log(err)) // {error: err.message}
// 2.
// Using await (provided we are working inside an asyn function)
try {
const result = await main()
console.log(result) // {result: data}
} catch (err) {
console.log(err) // {error: err.message}
}
Try to avoid combining both methods of handling asynchronous operations as a best practice.
I am having a Lambda function, in which I am taking a parameter from Query String and using it to call a downstream service that will inturn return a promise. I am unable to send back the response received by the Promise.
This is my Lambda function:
export const handler = async (event) => {
let sessionId = event.queryStringParameters.sessionId;
const testClient = new Client.WebStoreClient(config);
const sessionResponse = getSession(testClient, sessionId);
sessionResponse.then(function (result) {
console.log(result);
});
const response = {
statusCode: 200,
body: JSON.stringify(sessionResponse),
};
return response;
};
async function getSession(testClient, sessionId) {
let data = await testClient.getSession(sessionId, headers)
.then((response) => response.body);
return data;
}
When I am executing this, I am getting a null response: { statusCode: 200, body: '{}' } from the Lambda function output. I tried to console log the value that I have gotten and was receiving this: Promise { <pending> }
I am suspecting this to be a Javascript/Typescript Promise issue which I am slightly neglecting. Any help is appreciated here. Thanks in advance.
In your response, you are trying to stringify the promise instead of the promise value and it gives you an empty object as a String.
You can try it:
JSON.stringify(Promise.resolve(1)) // output is "{}"
You can only access the value of a Promise from inside the callback you pass to then.
Think of it this way: inside of then is the future, the surrounding code is the present. You can't reference future data in the present.
You probably wanted to write
return sessionResponse.then(function (result) {
return {
statusCode: 200,
body: JSON.stringify(result),
}
});
The above returns a promise containing your response.
You could have also just written getSession this way
function getSession(testClient, sessionId) {
return testClient.getSession(sessionId, headers)
.then((response) => response.body);
}
I suggest that you practice using promises without async/await
An alternative to geoffrey's answer, note that both of your functions are async, meaning you can use the same await keyword you used in getSession().
export const handler = async (event) => {
// ...
const sessionResponse = await getSession(testClient, sessionId);
// ^
console.log(sessionResponse);
const response = {
statusCode: 200,
body: JSON.stringify(sessionResponse),
};
return response;
};
async function getSession(testClient, sessionId) {
let data = await testClient.getSession(sessionId, headers)
.then((response) => response.body);
return data;
}
This is roughly the equivalent of...
export const handler = (event) => {
// ...
return getSession(testClient, sessionId).then((data) => {
console.log(data);
const response = {
statusCode: 200,
body: JSON.stringify(sessionResponse),
};
return response;
});
};
function getSession(testClient, sessionId) {
return testClient
.getSession(sessionId, headers)
.then((response) => response.body);
}
Note that your getSession() function is returning a Promise by virtue of async (and in the translated example above). Your handler() function is likewise returning a Promise, so whatever is calling it will need to use a .then() or be async itself to leverage an await. As you can see this asynchronous requirement cascades.
I am making my first hybrid web app using react hook.
I am faced with a timing problem with promise and context api.
Here are the logic I am facing with.
A function fetchApplications gets data from firebase firestore, it is defined in globalcontext.js (called in tableView)
tableView.js calls fetchApplications in useEffect.
fetchApplications is defined as promise function, I expect it will resolve(return) data until data fetched, it will resolove(return) object like {code:200, data:data}
problem
in the fetchData.code,
Cannot read property 'code' of undefined
Here is my code
In the tableview.js
React.useEffect(() => {
context.fetchApplications(auth.userId, "")
.then(function (fetchData) {
console.log("Fetched data: ", fetchData); ///this is undefined
if (fetchData.code !== 200) { /// error part
alert(fetchData.msg);
}
if (!fetchData.data) {
alert("No applications");
}
setsData(fetchData.data);
});
}, []);
In the GlobalContext.js
async function fetchApplications(userId, role) {
return new Promise((resolve, reject) => {
// resolve({ code: 200, data: "data" }); //If I add this code, it will be alright.
let dataArray = [];
let applicationRef = db
.collection("Users")
.doc(userId)
.collection("Applications");
applicationRef
.get()
.then(function (qs) {
qs.forEach(function (doc) { //doesn't work
console.log(doc.id, " => ", doc.data());
console.log(doc.size, " => ", typeof doc);
dataArray.push(doc.data());
});
return Promise.resolve(dataArray);
})
.then((data) => {
console.log("Global, Fetched Data", dataArray);
if (data) {
resolve({ code: 200, data: data });
}
return;
});
}).catch(function (error) {
reject({ code: 400, msg: "시스템에러 고객센터에 문의해주세요" });
});
}
wrote in codesendbox
If I was write the wrong way of promise, please let me know.
thanks
You're implementing a couple of bad practices and have some major issues. For starters, fetchApplications is marked as async but you're returning a manually created promise which is quite overkill as your fetching code actually generates a promise which you should directly return. Furthermore:
.then(function (qs) {
qs.forEach(function (doc) { //doesn't work
console.log(doc.id, " => ", doc.data());
console.log(doc.size, " => ", typeof doc);
dataArray.push(doc.data());
});
return Promise.resolve(dataArray);
})
I am not sure what exactly "//doesn't work" should mean but return Promise.resolve(dataArray); won't cut it for you. You're already in the then chain, so you can't resolve anything from the main promise at this point. You should just pass the data to the next then callback as return dataArray;.
All in all, I will suggest ditching the then-ables syntax all together and migrate to async/await altogether:
async function fetchApplications(userId, role) {
try {
const dataArray = [];
const applicationRef = db
.collection("Users")
.doc(userId)
.collection("Applications");
const querySnapshot = await applicationRef.get();
querySnapshot.forEach(doc => {
dataArray.push(doc.data());
});
return {
code: 200,
data: dataArray
};
}
catch (error) {
return {
code: 400,
msg: '시스템에러 고객센터에 문의해주세요'
};
}
}
Then, in your react component/hook:
React.useEffect(() => {
const fetchApplicationDataAsync = async () => {
const result = await context.fetchApplications(auth.userId, "");
if (result.code !== 200) {
alert(result.msg);
}
if (!result.data) {
alert("No applications");
}
setsData(result.data);
}
fetchApplicationDataAsync();
}, [auth.userId]);
Another problem and bad practice is that you're not specifying your dependencies for the useEffect hook. You have 2 external dependencies: the auth.userId paramater and the ontext.fetchApplications function. We alleviate one of the problem by creating the fetch function in the body of useEffect itself. However, the auth.userId should go into the dependency array as stated above.
You have to check for fetchData to be defined before accessing its properties.
A short form would be
if (fetchData && fetchData.code !== 200){...}
Applied to your code:
React.useEffect(() => {
context.fetchApplications(auth.userId, "")
.then(function (fetchData) {
console.log("Fetched data: ", fetchData); ///this is undefined
if (fetchData && fetchData.code !== 200) { /// error part
alert(fetchData.msg);
}else {
alert("No applications");
}
setsData(fetchData.data);
});
}, []);
By calling then() on the fetchApplications() function, as follows, you pass to the callback the fullfilment value from the Promise returned by fetchApplications() (i.e. fetchData gets the value returned by fetchApplications()).
context.fetchApplications(auth.userId, "")
.then(function (fetchData) {...}
However, fetchApplications() returns a Promise that resolves with undefined because, actually, you don't return the Promises chain. This is why you get an error on fetchData.code.
Adapting fetchApplications() as follows (using await, since you use async) should do the trick (untested!):
async function fetchApplications(userId, role) {
try {
let dataArray = [];
let applicationRef = db
.collection('Users')
.doc(userId)
.collection('Applications');
const qs = await applicationRef.get();
qs.forEach(doc => {
console.log(doc.id, ' => ', doc.data());
console.log(doc.size, ' => ', typeof doc);
dataArray.push(doc.data());
});
return { code: 200, data: dataArray };
} catch (error) {
return { code: 400, msg: '시스템에러 고객센터에 문의해주세요' };
}
}
Note that in any case you return an object with a code property, so no more problem when doing fetchData.code.
I am updating my javascript skills with Promises, already have in place a library with XHR and callbacks to load and inject multiple files at once and only proceed if ALL of them succeeded.
I am trying to use Promise.all() and Fetch API to get a similar functionality but can't make it work: console.log('All the promises are resolved', values); always triggers no matter if some of the fetch promises failed.
I want to be able to execute the code below, and only proceed with nowInitialize function if all the files were able to be fetched, or throw error using catch() with the reason of the first file that failed
xRequire(['index.html', 'style.cds'])
.then(nowInitialize)
.catch(reason => 'One or more files failed to load' + reason)
style.cds will obviously fail
//TODO Handle file types appropriately
//TODO: Inject css, javascript files
function xRequire(files) {
let urls = [];
let promisesList = [];
let handleAllPromises;
//Populates urls with each file required
for(let i=0; i < files.length ; i++) {
urls.push(files[i]);
}
//Fetch each file in urls
urls.forEach( (url, i) => { // (1)
promisesList.push(
fetch(url)
.then(handleResponse)
.then(data => console.log(data))
.catch(error => console.log(error))
);
});
handleAllPromises = Promise.all(promisesList);
handleAllPromises.then(function(values) {
console.log('All the promises are resolved', values);
});
handleAllPromises.catch(function(reason) {
console.log('One of the promises failed with the following reason', reason);
});
}
function handleResponse(response) {
let contentType = response.headers.get('content-type');
console.log('Requested Info: ' + contentType);
if (contentType.includes('application/json')) {
return handleJSONResponse(response);
} else if (contentType.includes('text/html')) {
return handleTextResponse(response);
} else if (contentType.includes('text/css')) {
return handleTextResponse(response);
} else if (contentType.includes('application/javascript')) {
return handleTextResponse(response);
} else {
throw new Error(`Sorry, content-type ${contentType} not supported`);
}
}
function handleJSONResponse(response) {
return response.json()
.then(json => {
if (response.ok) {
return json;
} else {
return Promise.reject(Object.assign({}, json, {
status: response.status,
statusText: response.statusText
}));
}
});
}
function handleTextResponse(response) {
return response.text()
.then(text => {
if (response.ok) {
return text;
} else {
return Promise.reject({
status: response.status,
statusText: response.statusText,
err: text
});
}
});
}
Can you just rewrite it as async-await code? Here is a rough idea of the typical flow:
const [data1, data2, data3] = await Promise.all([
fetch(url1),
fetch(url2),
fetch(url3),
]);
In other words, Promise.all() returns the promise to all the data that is returned from your multiple fetch() functions.
Then, if you put this into a try-catch, you can handle the rejection as well:
try {
const [data1, data2, data3] = await Promise.all([
fetch(url1),
fetch(url2),
fetch(url3),
]);
// Now you can process the data:
[data1, data2, data3].map(handleResponse);
} catch (error) {
console.log('Error downloading one or more files:', error);
}
If you want to loop with async-await, you can do that:
const promises = [];
for (const url of [url1, url2, url3, url4]) {
promises.push(fetch(url));
}
const [data1, data2, data3, data4] = await Promise.all(promises);
There are two problems. First, you need to return the Promise.all call from xRequire in order to consume it in your xRequire(..).then:
return Promise.all(promisesList);
Also, when you use .catch, if a Promise is initially rejected, it will go into the catch block, do whatever code is there, and then the Promise chain will resolve (not reject) to whatever the catch block returns. If you want to percolate errors up the Promise chain, put your catch at the point in the chain at which you want to detect errors:
urls.forEach( (url, i) => { // (1)
promisesList.push(
fetch(url)
.then(handleResponse)
.then(data => console.log(data))
// no catch here
);
});
I would suggest putting your catch only in the caller of xRequire, that way it will see all errors. Your xRequire function can be reduced to:
xRequire(['index.html', 'style.cds'])
.then(nowInitialize)
.catch(reason => 'One or more files failed to load' + reason)
function xRequire(files) {
return Promise.all(
urls.map(handleResponse)
);
}
If you want the body of xRequire to be able to see errors, but you also want to percolate errors up the Promise chain, throw an error in a catch inside xRequire, so that the Promise it resolves to will reject, rather than resolve:
function xRequire(files) {
return Promise.all(
urls.map(handleResponse)
)
.catch((err) => {
console.log('There was an error: ' + err);
throw err;
})
}
I finally solved it in this way --with the only quirk i've found so far: files argument always needs to be an array, therefore always needs brackets when calling the function--
xRequire(['my-file'])
.then(handle success)
.catch(handle error);
async function xRequire(files) {
let promises = [];
let receivedData;
//Iterate over files array and push results of fetch to promises array
files.map(x => promises.push(fetch(x)));
//populate receivedData array from promises array
receivedData = await Promise.all(promises);
//iterate over receivedData to handle each response accordingly
return receivedData.map(x => handleResponse(x));
}
I have the following functions with promises:
const ajaxRequest = (url) => {
return new Promise(function(resolve, reject) {
axios.get(url)
.then((response) => {
//console.log(response);
resolve(response);
})
.catch((error) => {
//console.log(error);
reject();
});
});
}
const xmlParser = (xml) => {
let { data } = xml;
return new Promise(function(resolve, reject) {
let parser = new DOMParser();
let xmlDoc = parser.parseFromString(data,"text/xml");
if (xmlDoc.getElementsByTagName("AdTitle").length > 0) {
let string = xmlDoc.getElementsByTagName("AdTitle")[0].childNodes[0].nodeValue;
resolve(string);
} else {
reject();
}
});
}
I'm trying to apply those functions for each object in array of JSON:
const array = [{"id": 1, "url": "www.link1.com"}, {"id": 1, "url": "www.link2.com"}]
I came up with the following solution:
function example() {
_.forEach(array, function(value) {
ajaxRequest(value.url)
.then(response => {
xmlParser(response)
.catch(err => {
console.log(err);
});
});
}
}
I was wondering if this solution is acceptable regarding 2 things:
Is it a good practice to apply forEach() on promises in the following matter.
Are there any better ways to pass previous promise results as parameter in then() chain? (I'm passing response param).
You can use .reduce() to access previous Promise.
function example() {
return array.reduce((promise, value) =>
// `prev` is either initial `Promise` value or previous `Promise` value
promise.then(prev =>
ajaxRequest(value.url).then(response => xmlParser(response))
)
, Promise.resolve())
}
// though note, no value is passed to `reject()` at `Promise` constructor calls
example().catch(err => console.log(err));
Note, Promise constructor is not necessary at ajaxRequest function.
const ajaxRequest = (url) =>
axios.get(url)
.then((response) => {
//console.log(response);
return response;
})
.catch((error) => {
//console.log(error);
});
The only issue with the code you provided is that result from xmlParser is lost, forEach loop just iterates but does not store results. To keep results you will need to use Array.map which will get Promise as a result, and then Promise.all to wait and get all results into array.
I suggest to use async/await from ES2017 which simplifies dealing with promises. Since provided code already using arrow functions, which would require transpiling for older browsers compatibility, you can add transpiling plugin to support ES2017.
In this case your code would be like:
function example() {
return Promise.all([
array.map(async (value) => {
try {
const response = await ajaxRequest(value.url);
return xmlParser(response);
} catch(err) {
console.error(err);
}
})
])
}
Above code will run all requests in parallel and return results when all requests finish. You may also want to fire and process requests one by one, this will also provide access to previous promise result if that was your question:
async function example(processResult) {
for(value of array) {
let result;
try {
// here result has value from previous parsed ajaxRequest.
const response = await ajaxRequest(value.url);
result = await xmlParser(response);
await processResult(result);
} catch(err) {
console.error(err);
}
}
}
Another solution is using Promise.all for doing this, i think is a better solution than looping arround the ajax requests.
const array = [{"id": 1, "url": "www.link1.com"}, {"id": 1, "url": "www.link2.com"}]
function example() {
return Promise.all(array.map(x => ajaxRequest(x.url)))
.then(results => {
return Promise.all(results.map(data => xmlParser(data)));
});
}
example().then(parsed => {
console.log(parsed); // will be an array of xmlParsed elements
});
Are there any better ways to pass previous promise results as
parameter in then() chain?
In fact, you can chain and resolve promises in any order and any place of code. One general rule - any chained promise with then or catch branch is just new promise, which should be chained later.
But there are no limitations. With using loops, most common solution is reduce left-side foldl, but you also can use simple let-variable with reassign with new promise.
For example, you can even design delayed promises chain:
function delayedChain() {
let resolver = null
let flow = new Promise(resolve => (resolver = resolve));
for(let i=0; i<100500; i++) {
flow = flow.then(() => {
// some loop action
})
}
return () => {
resolver();
return flow;
}
}
(delayedChain())().then((result) => {
console.log(result)
})