I've been reading through the Asynchronous Programming chapter of Eloquent JavaScript and came across this async bug problem. The solution offered here was to do a .join() at the end, but I came across another solution through using a blocked scoped const. The problem is I can't figure out why it fixes the bug, if someone could offer an explanation it would be much appreciated.
const resolveName = (fruitname) => {
return new Promise((resolve) => {
resolve(fruitname);
});
};
async function countLetters(fruitList) {
let result = "";
await Promise.all(
fruitList.map(async (fruit) => {
result += fruit + ": " + (await resolveName(fruit)).length + "\n";
// Fix: storing the string into a variable fixes this.
// const s = fruit + ": " + (await resolveName(fruit)).length + "\n";
// result += s;
})
);
return result;
}
const arr = ["apple", "banana", "cherry"];
const p = countLetters(arr);
p.then((r) => console.log(r));
I've been reading through the Asynchronous Programming chapter of Eloquent JavaScript and came across this async bug problem. The solution offered here was to do a .join() at the end, but I came across another solution through using a blocked scoped const. The problem is I can't figure out why it fixes the bug, if someone could offer an explanation it would be much appreciated.
const resolveName = (fruitname) => {
return new Promise((resolve) => {
resolve(fruitname);
});
};
async function countLetters(fruitList) {
let result = "";
await Promise.all(
fruitList.map(async (fruit) => {
result += fruit + ": " + (await resolveName(fruit)).length + "\n";
// Fix: storing the string into a variable fixes this.
// const s = fruit + ": " + (await resolveName(fruit)).length + "\n";
// result += s;
})
);
return result;
}
const arr = ["apple", "banana", "cherry"];
const p = countLetters(arr);
p.then((r) => console.log(r));
I've tried debugging and scouring SO for a solution. The expected and actual output is as below:
Expected:
apple: 5 banana: 6 cherry: 6
Actual:
cherry: 6
Here's a link to a runnable that reproduces the bug.
Share Improve this question edited yesterday Nick Parsons 50.8k6 gold badges57 silver badges75 bronze badges asked yesterday AnantaraAnantara 311 silver badge7 bronze badges New contributor Anantara is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct. 02 Answers
Reset to default 3The main difference is to do with when result
is read and used in both of your callbacks. In the first buggy code, it's read immediately when the .map()
callback fires, which at that time it's an empty string, and so you end up concatenating to an empty string and only see the last result. With your second fixed code, you're reading the value of result
after the async code has finished, allowing each of your callbacks to update the shared result
value and see the latest value of result
as each callback resumes its execution one by one for each async call that was made.
For your buggy code, JavaScript evaluates the expression below from left to right:
// vvvvvvvvvvvvvvvvvvvvvv --- synchronous
result = result + (fruit + ": " + (await resolveName(fruit)).length + "\n");
// ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ --- aysnc
Each iteration of your .map()
runs synchronously until it hits the async call, where it suspends the current callback's exeuction. So in the above line, the concatenation of result + fruit + ": "
occurs synchronously and so it uses the original empty value of result
at the time the .map()
callback runs. However, the entire concatenation from the above expression can't complete and update the result
variable until the resolveName()
promise has resolved and the suspended callback's execution resumes. Due to how JavaScript handles promise resolutions (via the microtask queue), this only occurs after all the synchronous map iterations have completed and .map()
has returned, meaning that result
is an empty string for all the map iterations when result += ...
is processed.
With your code, things a little different, as result
isn't involved yet:
const s = fruit + ": " + (await resolveName(fruit)).length + "\n";
again, fruit + ": "
is computed synchronously, but the remainder of the expression can't be computed until the await
is processed (which only happens after all map iterations have occurred). So the callback gets suspended and the remaining code which uses and updates result
(ie: result += s
) is only computed and run after the Promises have resolved and been processed. Thus, as the promises for "apple"
, "banna"
, and "cherry"
are resolved, their associated suspended callback tasks are processed one by one off the micro-task queue, allowing your callback functions to resume and update the shared result
variable. So first, the callback for apple
resumes and updates result
, then the callback for "bananna"
will resume and update the same result
variable, and then lastly the callback for "cherry"
will resume and update result
.
When an expression like
a += b
is evaluated, the value of a
is computed first, before any work is done on computing the value of b
.
In your case, the value of result
is computed in each of the iteration "branches" of the .map()
call (which should really be .forEach()
, but that's a separate issue). The initial value of result
is the empty string, so all three branches start with that empty string value.
When the right-hand side is then evaluated, each branch will yield a promise and wait for that asynchronous call to complete. When that happens, each branch will proceed with evaluating that expression. Because each branch already computed that empty string as the starting value of result
, the overall result of each branch will be just that one single fruit name, and the last one that completed will be the overall result.
By performing the +=
operation in a separate expression, each branch of the .map()
will be affected by the completion of the previous one. The first promise will resolve and the empty string will be replaced with the first fruit name. When the second fruit resolves, it will start with the updated value of result
, not the empty string.