1

I'm trying to understand why the following results happen. I bet there's something to do with the event loop and the fact that all sync code runs before async code, but I cannot apply that knowledge to this example.

// const axios = require('axios');
const array = [1, 2, 3];

const user = {};

const promises = array.map(async (item) => {
  console.log('start');
  user['id'] = item;
  const responseUser = await axios.get(
    `https://jsonplaceholder.typicode.com/todos/${user.id}`
  );
  console.log('middle');
  const responseUser2 = await axios.get(
    `https://jsonplaceholder.typicode.com/todos/${user.id}`
  );
  console.log('end result 2: ', responseUser2.data.id);
  console.log('end result: ', responseUser.data.id);
});

Promise.all(promises);
<script type="text/javascript" src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>

Results:

start
start
start
middle
middle
middle
end result 2: 3
end result: 3
end result 2: 3
end result: 2
end result 2: 3
end result: 1
22
  • All of those promises are going to run as fast as they can in parallel. You cannot share user and mutate it this way without some kind of mutex or lock because it produces a race condition. Commented Sep 11, 2021 at 6:09
  • @zero298 JavaScript programs are single-threaded: and in NodeJS and in the browser (at least) concurrently executing async Promises don't run JavaScript in parallel (though if they run internal host code, like for network IO, then that code in the host could run in parallel, but JS code itself will not). So you don't need any kind of locking to be thread-safe - though in this case, yes, mutating user['id'] is wrong, but it isn't a race-condition (specifically) in this context. Commented Sep 11, 2021 at 6:15
  • You need to add an await to Promise.all(promises);, so change your code to await Promise.all(promises); Commented Sep 11, 2021 at 6:17
  • 1
    Why are you even using map without a return? Use a for loop. Commented Sep 11, 2021 at 6:35
  • 1
    @Dai the term race condition is not limited to muli-threading. JavaScript engines are event driven and await/async behaves similar to cooperative multitasking. So as soon as you use await (or callbacks) the sequence and timing of the execution can change, and that can lead to race conditions. (For sure not that variables are modified by to cores at the same moment, but that’s not the requirement for a race condition) Commented Sep 11, 2021 at 6:47

1 Answer 1

4

This happens because .map will launch the callback for each array element synchronously. Those callbacks are asynchronous, and so each execution of it will return when having evaluated the first await expression. So you get a bunch of suspended executions which each will only continue later from a job in the promise job queue (asynchronously).

Note that your call of Promise.all will execute after all "start" outputs, but it actually isn't doing anything useful, as you don't do anything with the promise that it returns.

Each .map callback will synchronously launch the first HTTP request with the intended user.id (which is the current item from the array), but that URL evaluation is the last synchronous code that executes. By the time the await-ed promises resolve, user.id will have the value of the last item in the array.

Asynchronously, each of the callback executions will then resume. At that time user.id already has the value of the last item in the array. These resumed executions will thus launch their second HTTP request with the last id (3). If your intention was that those second requests would use the same id as the one that preceded it in the code, then you'd have to pass item as URL query, not item.id.

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you! Do you happen to know why does each execution return when having evaluated the first await expression? Does it have anything to do with the fact that the map needs to return something and it somehow returns the first asynchronous call? How does it manage to still keep track of the other ones?
This is the behaviour of await. It makes the async function return a promise. Further execution of the async function will be triggered when the promise, on which the await is applied, resolves. This is not specific to the .map context you have here; it always works like that with await.
By running this code with map and a traditional for loop, I see that using map runs it much faster, even though each await actually waits for the promise to resolve. What is the difference then? It seems that, even though both ways you are waiting for each promise to finish, using map does run each loop iteration in parallel while the for loop runs each one after the other and actually waits all promises from previous iteration to finish running.
map does not wait. It just calls the first function, and when it returns (remember: when await is encountered), it immediately calls the next, ...etc. A for loop does not have a callback function, so any await in a for loop makes the function (in which the await is positioned .... which is the same function the for loop is in) return! Therefore the for loop will only do its next iteration when the awaited promise resolves and the function resumes.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.