123

Take the following loop:

for(var i=0; i<100; ++i){
    let result = await some_slow_async_function();
    do_something_with_result();
}
  1. Does await block the loop? Or does the i continue to be incremented while awaiting?

  2. Is the order of do_something_with_result() guaranteed sequential with regard to i? Or does it depend on how fast the awaited function is for each i?

Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
smerg
  • 1,506
  • 3
  • 10
  • 14
  • 12
    Have you actually tried it out? – Bergi Jun 07 '17 at 10:33
  • 5
    Yes, I'm getting a sequential result. I wasn't sure if it was a coincidence (the async function is actually fast). I wasn't sure whether to push all the async function calls into an array and then do a single `await Promise.all(arr)`, or if this form was correct and something else was hindering the desired asynchronicity. If I do a single `await` for all of them, then I'd have to get into `Promise.map` in order to handle each one. It makes me question if `.then` is better than async/await in this situation. – smerg Jun 07 '17 at 10:37
  • JS is deterministic. Either the function is asynchronous or it is not, it never depends on "how fast" something is executed. Regarding `Promise.all`, that does something different - whether it is still correct (or even more desirable) depends on your requirements. – Bergi Jun 07 '17 at 10:41
  • The time for a function to run is non-deterministic when doing an `async` operation that involves an external resource e.g. database, file i/o. – smerg Jun 07 '17 at 10:44
  • 2
    Yes, but as long as it is asynchronous, it will never call its callbacks immediately, and always run to completion of the sync code first. – Bergi Jun 07 '17 at 10:49

8 Answers8

122
  1. Does await block the loop? Or does the i continue to be incremented while awaiting?

"Block" is not the right word, but yes, i does not continue to be incremented while awaiting. Instead the execution jumps back to where the async function was called, providing a promise as return value, continuing the rest of the code that follows after the function call, until the code stack has been emptied. Then when the awaiting is over, the state of the function is restored, and execution continues within that function. Whenever that function returns (completes), the corresponding promise -- that was returned earlier on -- is resolved.

  1. Is the order of do_something_with_result() guaranteed sequential with regard to i? Or does it depend on how fast the awaited function is for each i?

The order is guaranteed. The code following the await is also guaranteed to execute only after the call stack has been emptied, i.e. at least on or after the next microtask can execute.

See how the output is in this snippet. Note especially where it says "after calling test":

async function test() {
    for (let i = 0; i < 2; i++) {
        console.log('Before await for ', i);
        let result = await Promise.resolve(i);
        console.log('After await. Value is ', result);
    }
}

test().then(_ => console.log('After test() resolved'));

console.log('After calling test');
trincot
  • 317,000
  • 35
  • 244
  • 286
  • The stack explanation reminds me that `async/await` was until recently implemented using generators/`yield`. Thinking of it this way makes everything clearer. I'll accept this answer. – smerg Jun 07 '17 at 10:55
  • 2
    @smorgs The stack explanation actually has more to do with standard promise `then` behaviour than with generators. Only the "jump back to where it was called" and "state of the function is restored" are `yield`. – Bergi Jun 07 '17 at 11:01
  • Understood; I find the stack model plain to see with `.then` but not so with `await`, so thinking of it as a `yield` resolves my confusion. – smerg Jun 07 '17 at 11:04
20

As @realbart says, it does block the loop, which then will make the calls sequential.

If you want to trigger a ton of awaitable operations and then handle them all together, you could do something like this:

const promisesToAwait = [];
for (let i = 0; i < 100; i++) {
  promisesToAwait.push(fetchDataForId(i));
}
const responses = await Promise.all(promisesToAwait);
Kris Selbekk
  • 7,438
  • 7
  • 46
  • 73
  • 5
    This answer doesn't actually answer the question – Liam Jun 07 '17 at 10:41
  • This makes no sense. Your `promisesToAwait` array will never contain promises. – Bergi Jun 07 '17 at 10:44
  • 1
    It does not block the loop, but it does not execute as expected too. – Allan Felipe Murara Aug 26 '18 at 15:36
  • realbart -- to whom's answer you refer -- deleted their answer, as the claim that the loop is being blocked is at least misleading, if not: just wrong. Nothing is blocked. Execution continues, just not in the loop. Also: *"make the calls sequential"*: this is not very clear: there is no other way to execute JS code than sequentially. – trincot Feb 02 '22 at 15:06
  • This actually worked for me. I had a loop that called a promise returning function and couldn't figure out why it stopped after one iteration. This fixed it. – blutuu Mar 15 '22 at 06:10
19

You can test async/await inside a "FOR LOOP" like this:

(async  () => {
        for (let i = 0; i < 100; i++) {
                await delay();
                console.log(i);
        }
})();

function delay() {
        return new Promise((resolve, reject) => {
                setTimeout(resolve, 100);
        });
}
mzalazar
  • 6,206
  • 3
  • 34
  • 31
2

async functions return a Promise, which is an object that will eventually "resolve" to a value, or "reject" with an error. The await keyword means to wait until this value (or error) has been finalized.

So from the perspective of the running function, it blocks waiting for the result of the slow async function. The javascript engine, on the other hand, sees that this function is blocked waiting for the result, so it will go check the event loop (ie. new mouse clicks, or connection requests, etc.) to see if there are any other things it can work on until the results are returned.

Note however, that if the slow async function is slow because it is computing lots of stuff in your javascript code, the javascript engine won't have lots of resources to do other stuff (and by doing other stuff would likely make the slow async function even slower). Where the benefit of async functions really shine is for I/O intensive operations like querying a database or transmitting a large file where the javascript engine is well and truly waiting on something else (ie. database, filesystem, etc.).

The following two bits of code are functionally equivalent:

let result = await some_slow_async_function();

and

let promise = some_slow_async_function(); // start the slow async function
// you could do other stuff here while the slow async function is running
let result = await promise; // wait for the final value from the slow async function

In the second example above the slow async function is called without the await keyword, so it will start execution of the function and return a promise. Then you can do other things (if you have other things to do). Then the await keyword is used to block until the promise actually "resolves". So from the perspective of the for loop it will run synchronous.

So:

  1. yes, the await keyword has the effect of blocking the running function until the async function either "resolves" with a value or "rejects" with an error, but it does not block the javascript engine, which can still do other things if it has other things to do while awaiting

  2. yes, the execution of the loop will be sequential

There is an awesome tutorial about all this at http://javascript.info/async.

Casey
  • 490
  • 7
  • 11
  • *"sees that this function is blocked waiting for the result, so it will go check the event loop"*: no, this is a misrepresentation of what happens. The function actually *returns* when an `await` occurs, and code execution continues after the function call. – trincot Jan 29 '20 at 07:52
  • @trincot the nuance is specifically 'which' code execution continues after the function call. Is it 1) the code inside the async function that continues, or 2) the code in the calling function after the await, or 3) any other activities enqueued onto the event loop. From the perspective of the calling function it appears to be blocked (waiting for the promise to resolve), from the perspective of the async function it is running normally (but possibly waiting on external I/O or something), and from the perspective of Node it will check the event loop to see if there are other things to do. – Casey Jan 29 '20 at 11:15
  • @trincot that is also why I said "has the 'effect' of blocking the running function". It is not really blocked, and Node will do other stuff from the event loop (as well as continuing to work its way through execution of the async function), but from the perspective of the calling function (not the async function that was called) it will "appear" to be blocked. – Casey Jan 29 '20 at 11:17
  • 1
    Mentioning that Node "will check the event loop" is misleading. Node will ***not*** check the event loop when `await` is encountered. The function will return, and code execution will continue, like it would after *any* function call. The event loop only plays a role when the call stack is empty, which it isn't when `await` is encountered. BTW: this question is not specifically about Node, but JavaScript in general. – trincot Jan 29 '20 at 12:02
  • 1
    You also wrote *"so it will go check the event loop (ie. new mouse clicks, or connection requests, etc.)"*. This is not true. (1) code execution continues *without* regard of the event loop, and (2) even when there are events like mouse clicks, they have no precedence over promise jobs, which are in a different queue ("Promise Job Queue"). A promise resolution (which would restore the function context at the `await`) has precedence over mouse clicks and other agent-driven events. – trincot Jan 29 '20 at 12:07
1

No Event loop isn't blocked, see example below

function sayHelloAfterSomeTime (ms) {
  return new Promise((resolve, reject) => {
    if (typeof ms !== 'number') return reject('ms must be a number')
    setTimeout(() => { 
      console.log('Hello after '+ ms / 1000 + ' second(s)')
      resolve()  
    }, ms)
  })
}

async function awaitGo (ms) {
   await sayHelloAfterSomeTime(ms).catch(e => console.log(e))
   console.log('after awaiting for saying Hello, i can do another things  ...')
}

function notAwaitGo (ms) {
 sayHelloAfterSomeTime(ms).catch(e => console.log(e))
    console.log('i dont wait for saying Hello ...')
}

awaitGo(1000)
notAwaitGo(1000)
console.log('coucou i am event loop and i am not blocked ...')
KBH
  • 315
  • 3
  • 9
1

Here is my test solution about this interesting question:

import crypto from "crypto";

function diyCrypto() {
    return new Promise((resolve, reject) => {
        crypto.pbkdf2('secret', 'salt', 2000000, 64, 'sha512', (err, res) => {
            if (err) {
                reject(err)
                return 
            }
            resolve(res.toString("base64"))
        })
    })
}

setTimeout(async () => {
    console.log("before await...")
    const a = await diyCrypto();
    console.log("after await...", a)
}, 0);

setInterval(() => {
    console.log("test....")
}, 200);

Inside the setTimeout's callback the await blocks the execution. But the setInterval is keep runnning, so the Event Loop is running as usual.

Chris Bao
  • 2,418
  • 8
  • 35
  • 62
1

Let me clarify a bit because some answers here have some wrong information about how Promise execution works, specifically when related to the event loop.

In the case of the example, await will block the loop. do_something_with_result() will not be called until await finishes it's scheduled job.

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await#handling_asyncawait_slowdown

As for the other points, Promise "jobs" run before the next event loop cycle, as microtasks. When you call Promise.then() or the resolve() function inside new Promise((resolve) => {}), you creating a Job. Both await and async are wrapper, of sorts, for Promise, that will both create a Job. Microtasks are meant to run before the next event loop cycle. That means adding a Promise Job means more work before it can move on to the next event loop cycle.

Here's an example how you can lock up your event loop because your promises (Jobs) take too long.

let tick = 0;
let time = performance.now();
setTimeout(() => console.log('Hi from timeout'), 0);
const tock = () => console.log(tick++);
const longTask = async () => {
  console.log('begin task');
  for(let i = 0; i < 1_000_000_000; i++) {
    Math.sqrt(i);
  }
  console.log('done task');
}
requestAnimationFrame(()=> console.log('next frame after', performance.now() - time, 'ms'));
async function run() {
  await tock();  
  await tock();
  await longTask(); // Will stall your UI
  await tock();   // Will execute even though it's already dropped frames
  await tock();   // This will execute too
}
run();
// Promise.resolve().then(tock).then(tock).then(longTask).then(tock).then(tock);

In this sample, 5 total promises are created. 2 calls for tock, 1 for longTask and then 2 calls for tock. All 5 will run before the next event loop.

The execution would be:

  • Start JS execution
  • Execute normal script
  • Run 5 scheduled Promise jobs
  • End JS execution
  • Event Loop Cycle Start
  • Request Animation Frame fire
  • Timeout fire

The last line commented line is scheduling without async/await and results in the same.

Basically, you will stall the next event loop cycle unless you tell your JS execution where it can suspend. Your Promise jobs will continue to run in the current event loop run until it finishes its call stack. When you call something external, (like fetch), then it's likely using letting the call stack end and has a callback that will resolve the pending Promise. Like this:

function waitForClick() {
  return new Promise((resolve) => {
    // Use an event as a callback;
    button.onclick = () => resolve();
    // Let the call stack finish by implicitly not returning anything, or explicitly returning `undefined` (same thing).
    // return undefined; 
  })
}

If you have a long job job that want to complete, either use a Web Worker to run it without pausing, or insert some pauses with something like setTimeout() or setImmediate().

Reshaping the longTask function, you can do something like this:

const longTask = async () => {
  console.log('begin task');
  for(let i = 0; i < 1_000_000_000; i++)
    if (i && i % (10_000_000) === 0) {
      await new Promise((r) => setTimeout(r,0));
    }
    Math.sqrt(i);
  console.log('done task');
}

Basically, instead of doing 1 billion records in one shot, you only do 10 million and then wait until the next event (setTimeout) to run the next one. The bad here is it's slower because of how much you hand back to the event loop. Instead, you can use requestIdleCallback() which is better, but still not as good as multi-threading via Web Workers.

But be aware that just slapping on await or Promise.resolve().then() around a function won't help with the event loop. Both will wait until the function returns with either a Promise or a value before letting up for the event loop. You can mostly test by checking to see if the function you're calling returns an unresolved Promise immediately.

ShortFuse
  • 5,970
  • 3
  • 36
  • 36
  • "*a good number of answers are wrong*" - are they? There's only one plainly wrong answer, which is downvoted multiple times. Btw the phrase "*before the next event loop*" doesn't make sense, there's only one event loop. Did you mean "before the next *iteration* of the event loop"? – Bergi Feb 09 '22 at 01:11
  • @Bergi Every answer that mention the `event loop` state Promises don't block event loop, or Javascript will return to the event loop. It can which is misleading (as pointed out in the comments of other answers) or just wrong. There's no need to fingerpoint, so I kept it vague. "next loop of the event loop" or "current event loop loop" was more technically correct, but sounded confusing, but I'll edit it. – ShortFuse Feb 09 '22 at 14:52
  • Ah that's what you refer to. Yeah, you're answer is more accurate, but imo it wouldn't be wrong to say that "*`await` does not block the main thread like a synchronous function so it has a chance to return to the event loop*". – Bergi Feb 10 '22 at 10:34
  • 1
    Btw, "*When you construct a Promise explicitly (`new Promise`), you are adding a Job that will be run at the end of the current event loop cycle*" is wrong, the executor function runs immediately – Bergi Feb 10 '22 at 10:35
  • @Bergi Thank you very much! I changed the the point about `new Promise` with the fact that a Job is scheduled when `resolve()` is called, and made wording sound a bit more friendly. I also added few more references. Let me know if you see anything else. – ShortFuse Feb 10 '22 at 16:12
  • When you start your answer with such a bold statement, it is all the more important that your own answer phrases things correctly: *"Your Promise will continue to run"*: promises don't run. They are objects, not functions. You can say something about the *state* of a promise, or the transition to a different state, but you cannot say that a promise "runs". – trincot Nov 27 '22 at 09:10
  • @trincot I was clearly talking about Promise jobs which I explained the preceding paragraphs. The aggressive language isn't necessary, but I'll fix the typo. – ShortFuse Nov 28 '22 at 20:00
-5

Does await block the loop? Or does the i continue to be incremented while awaiting?

No, await won't block the looping. Yes, i continues to be incremented while looping.

Is the order of do_something_with_result() guaranteed sequential with regard to i? Or does it depend on how fast the awaited function is for each i?

Order of do_something_with_result() is guaranteed sequentially but not with regards to i. It depends on how fast the awaited function runs.

All calls to some_slow_async_function() are batched, i.e., if do_something_with_result() was a console then we will see it printed the number of times the loop runs. And then sequentially, after this, all the await calls will be executed.

To better understand you can run below code snippet:

async function someFunction(){
for (let i=0;i<5;i++){
 await callAPI();
 console.log('After', i, 'th API call');
}
console.log("All API got executed");
}

function callAPI(){
setTimeout(()=>{
console.log("I was called at: "+new Date().getTime())}, 1000);
}

someFunction();

One can clearly see how line console.log('After', i, 'th API call'); gets printed first for entire stretch of the for loop and then at the end when all code is executed we get results from callAPI().

So if lines after await were dependent on result obtained from await calls then they will not work as expected.

To conclude, await in for-loop does not ensure successful operation on result obtained from await calls which might take some time to finish.

In node, if one uses neo-async library with waterfall, one can achieve this.

robe007
  • 3,523
  • 4
  • 33
  • 59
DeeKay
  • 1
  • 2
  • 3
    It is because your callAPI function doesn't return a promise so `await callAPI()` return immediately. If you change your `callAPI()` to return a promise then it will work. – Henry Liu Jan 22 '19 at 16:15