I have some code that iterates over a list of objects representing files. In the loop there is a call to 'getRequest()' which returns type 'Promise<Response>' and then a call to '.text()' which returns 'Promise<string>'. I have a working solution that first gets all the promises into a list and then calls Promise.all() and the documents are fetched correctly.
However when I try another solution where I am directly waiting for the promise to return and then using the values, the documents are not populated correctly.
//----------------------------- WORKING SOLUTION ---------------------------------------
async function parseDocumentation(config: Map<string, ConfigElement[]>) {
const promises: Promise<Response>[] = [];
const documents: Documentation[] = [];
config.forEach((sectionDef: ConfigElement[]) => {
sectionDef.forEach(entry => {
if (!entry.subsection) {
// getRequest: (endpointUrl: string) => Promise<Response>
promises.push(getRequest(entry.source));
}
});
});
try {
await Promise.all(promises).then(async responses => {
for (const response of responses) {
if (!response.ok) {
return Promise.reject();
} else {
const document = {source: '', markDown: '' };
document.markDown = await entry.text(); // Body.text(): Promise<string>
document.source = response.url;
documents.push(document);
}
}
});
} catch (e) {
Dialog.error('Failed to load page.', e);
}
}
//----------------------------- NON WORKING SOLUTION ---------------------------------------
async function parseDocumentation(config: Map<string, ConfigElement[]>) {
const documents: Documentation[] = [];
config.forEach((sectionDef: ConfigElement[], sectionName: string) => {
sectionDef.forEach(async entry => {
if (!entry.subsection) {
entry.hash = 'some_hash_value_' sectionName;
await getRequest(entry.source).then(async response => {
if (!response.ok) {
return Promise.reject();
} else {
const document = {source: '', markDown: '' };
await response.text().then(async text => {
// would like to save entry.hash to document here
document.markDown = text;
document.source = response.url;
documents.push(document);
});
}
});
}
});
});
}
Can anyone explain what is happening? I think I mirrored the logic correctly but for some reason the second variation does not populate documents correctly.
CodePudding user response:
The problem is with .forEach(async ....
construct: the callback is an asynchronous function, and the first await
in it will return/suspend that callback function (not the outer function), making the forEach
loop continue with the next iterations. The asynchronous code (below await
) in the callbacks will only execute when the callstack is empty, so the forEach
will already have finished its iterations before that happens.
To solve this, use a loop construct that doesn't need a callback, like for..of
. Then the await
s operate on the parseDocumentation
function itself:
async function parseDocumentation(config: Map<string, ConfigElement[]>) {
const documents: Documentation[] = [];
for (const [sectionName: string, sectionDef: ConfigElement[]] of config) {
for (const entry: ConfigElement of sectionDef) {
// ...
}
}
}
Not the question, but try to get the most out of await
by getting the promised value from it -- avoiding a then
chain:
async function parseDocumentation(config: Map<string, ConfigElement[]>) {
const documents: Documentation[] = [];
for (const [sectionName: string, sectionDef: ConfigElement[]] of config) {
for (const entry: ConfigElement of sectionDef) {
if (entry.subsection) continue;
entry.hash = 'some_hash_value_' sectionName;
const response = await getRequest(entry.source);
if (!response.ok) throw new Error("One of the responses was not OK");
documents.push({
source: response.url,
markDown: await response.text(),
});
}
}
}
Note that throwing an error in an async
function will reject the function's returned promise with as reason the error message.
CodePudding user response:
You should do :
let text = await response.text();