Home > Enterprise >  Why does this loop repeat each iteration twice?
Why does this loop repeat each iteration twice?

Time:02-26

The function below prints each number twice. Could someone explain how it works? I tried debugging but all I can see is that the value of i only increases on every second iteration.

async function run(then) {
    for (let i = 1; i <= 10; i  ) {
        console.log(i);
        then = await { then };
    }
}

run(run);

Concretely speaking, there are two things I don't understand.

  • Why does i not increase on every single iteration?
  • What does then = await { then }; exactly do? My first guess was that it would wait for a nested async call to run to finish before moving on to the next iteration, but this does not seem to be the case.

CodePudding user response:

We can make this a bit clearer with minor re-write to include logging:

async function run(callback) {
    let then = callback;
    for (let i = 1; i <= 10; i  ) {
        console.log(callback === run ? "A" : "B", i);
        then = await { then };
    }
}

run(run);
.as-console-wrapper { max-height: 100% !important; }

This shows there are actually two loops started. For simplicity just called A and B. They log and await which means that their logs interleave and lead to A 1, B 1, A 2, B 2, etc.

This happens because of the first statement: run(run). Which passes the same function as a callback to itself. This does not call the callback but it is the first step to unravelling this.


The next step to understanding what is happening is await. You can await any value and in most cases if it's not a promise, it doesn't matter. If you have await 42; it just pretends the value was Promise.resolve(42) and continues the operation immediately with the next tick. That is true for most non-promises. The only exception is thenables - objects which have a .then() method.

When a thenable is awaited, its then() method is called:

const thenable = {
  then() {
    console.log("called");
  }
};

(async () => {
  await thenable;
})()

Which then explains the await { then } statement. This uses the shorthand for { then: then } where then is the callback passed to run. Thus it creates a thenable object which, when awaited, will execute the callback.

This means that the first time run() is executed and on the first iteration of the loop A the code is effectively await { then: run } which will execute run again which then starts loop B.

The value of then is overridden each time, hence why it only ever runs two loops in parallel, rather than more.


There is more to thenables that is relevant to fully grasp this code. I showed a simple one before which just shows that awaiting it calls the method. However, in reality await thenable will call .then() with two parameters - functions that can be called for success and failure. In the same way that the Promise constructor does it.

const badThenable = {
  then() {
    console.log("bad called");
  }
};

(async () => {
  await badThenable;
  console.log("never reached");
})();

const goodThenable = {
  then(resolve, reject) { //two callbacks
    console.log("good called");
    resolve(); //at least one needs to be called
  }
};

(async () => {
  await goodThenable;
  console.log("correctly reached");
})();

This is relevant because run() expects a callback and when the await { then: run } executes it calls run(builtInResolveFunction) which then gets passed to the next await { then: builtInResolveFunction } which in turn resolves causes the a await to resolve.


With all this aside, the interleaved logging is just a factor of how tasks resolve:

(async () => {
  for (let i = 1; i <= 10; i  ){
    console.log("A", i);
    await Promise.resolve("just to force a minimal wait");
  } 
})();

(async () => {
  for (let i = 1; i <= 10; i  ) {
    console.log("B", i);
    await Promise.resolve("just to force a minimal wait");
  } 
})();

If there are two async functions running and there is nothing to really wait for:

  1. One would run until it reaches an await and will then be suspended.
  2. The other would run until it reaches an await and will then be suspended.
  3. Repeat 1. and 2. until there are no more awaits.
  • Related