2

According to the MDN documentation on await:

When an await is encountered in code (either in an async function or in a module), the awaited expression is executed, while all code that depends on the expression's value is paused and pushed into the microtask queue.

async function foo(name) {
  console.log(name, "start");
  console.log(name, "middle");
  console.log(name, "end");
}

foo("First");
foo("Second");

// First start
// First middle
// First end
// Second start
// Second middle
// Second end

In this case, the two async functions are synchronous in effect, because they don't contain any await expression. The three statements happen in the same tick.

This is clear BUT this:

However, as soon as there's one await, the function becomes asynchronous, and execution of following statements is deferred to the NEXT tick.

async function foo(name) {
  console.log(name, "start");
  await console.log(name, "middle");
  console.log(name, "end");
}

foo("First");
foo("Second");

// First start
// First middle
// Second start
// Second middle
// First end
// Second end

My question is why NEXT tick? Isn't it the same tick of the event loop? Microtask (all code that depends on the expression's value) will be taken from the microtask queue but NOT on the next tick?

AlexBor
  • 151
  • 2
  • 12
  • When code is pushed onto the queue, the only way it can execute is in a subsequent tick; that's what "tick" means. – Pointy Jun 22 '23 at 11:12
  • I thought that every time the event loop takes a full trip, we call it a tick? – AlexBor Jun 22 '23 at 11:20
  • Right, so when code after an `await` is pushed onto the microtask queue, it runs in the next event loop, not the current one. – Pointy Jun 22 '23 at 11:23
  • @AlexBor I would agree with you, it's the current event loop. IOW: `Sync Code`, `MacroTasksQueue`, then goes to next event loop. – Keith Jun 22 '23 at 11:43
  • 1
    Different documentations use different definitions of "tick". I have the impression MDN means that every pull from a job queue (no matter which type of queue it is), corresponds to a new tick, while Node documentation seems to reserve "tick" for when the **event** loop pulls something from the event queue, which for them does not include the queues that have the callbacks that will execute as part of the same task, like is the case for the PromiseJob queue. I think this is the source of confusion and reason for your question. – trincot Jun 22 '23 at 11:45
  • @trincot That might be the case actually – AlexBor Jun 22 '23 at 11:47
  • So if we add a task, setTimeout(() => console.log('Task'), 0), which will be executed in the next iteration of the Event loop, that would mean that code after `await` will be executed after that task right? I don't think so! At least this is how I understand Event loop in the browser. – AlexBor Jun 22 '23 at 12:19
  • Spanner in the works here, since the event loop is just a loop, you could say it's `At the end of the current loop`, or `the start of the next loop`, but not the `end of the next`.. :) But technically I believe it's the end of the current loop, because the event loop wait logic will be using optimised (very low CPU) wait logic so I would class that as the start. – Keith Jun 22 '23 at 13:02
  • For those who have done say, windows coding in C/C++ etc, This pseudo code might make sense.. `while (!terminated) { var msg = getMessage(); processMessage(msg); processMicroTasks(); }` – Keith Jun 22 '23 at 13:10

2 Answers2

2

My question is why NEXT tick?

The definition that Mozilla Contributors use for "tick" is slightly different from what the authors of the NodeJs documentation call "tick", and this might be the cause of confusion.

Isn't it the same tick of the event loop?

It is indeed executed in the same iteration of the event loop.

Microtask (all code that depends on the expression's value) will be taken from the microtask queue but NOT on the next tick?

then-callbacks (or their await function-resuming counterparts) are indeed taken from the microtask queue. Whether or not this is regarded as the same or different tick, depends on the definition of "tick".

References for the differing definitions

NodeJs Docs

This is what the NodeJs documentation defines as one phase of the event loop:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

Each box will be referred to as a "phase" of the event loop.

In the same document, a single iteration of this loop is identified as a "tick":

setImmediate() fires on the following iteration or 'tick' of the event loop

This means in one tick (iteration), there can be many scheduled callbacks that are executed, including callbacks in the microtask queue.

Mozilla Contributors

Mozilla Contributors define "event loop" in a more generic and simplistic way here, but when discussing microtasks they say something similar to the NodeJs documentation:

Each agent is driven by an event loop, which collects any user and other events, enqueuing tasks to handle each callback. It then runs any pending JavaScript tasks, then any pending microtasks, then performs any needed rendering and painting before looping again to check for pending tasks.

But this is not entirely correct, as the HTML Standard stipulates that one iteration of the event loop will pick and execute one(!) task, and will then process any pending microtasks. If there are more tasks, then they will be treated in the next iterations of the event loop, one task at a time.

The major difference comes with what they call a "tick", as seen in what you quoted, and also here:

myPromise
  .then((value) => `${value} and bar`)
  .then((value) => `${value} and bar again`)
  .then((value) => `${value} and again`)
  .then((value) => `${value} and again`)
  .then((value) => {
    console.log(value);
  })
  .catch((err) => {
    console.error(err);
  });

Note: For faster execution, all synchronous actions should preferably be done within one handler, otherwise it would take several ticks to execute all handlers in sequence.

What is called a tick here, refers to the engine initiating a single (then-) callback from the microtask queue, and in general, calling a callback from any queue that is monitored by the engine. For the authors of the NodeJs documentation those callbacks are all made in the same "tick".

As if this is not confusing enough, NodeJs includes a function called process.nextTick() to schedule the execution of a callback, and the corresponding queue of callbacks is actually processed within the same event loop iteration! So what's in a name...:

The process.nextTick() queue is always processed before the microtask queue within each turn of the Node.js event loop.

Conclusion

Your understanding of the process is correct, but the different definitions of the word "tick" by different authors is bringing confusion to the subject.

I would avoid the term all together.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • I think this is a great answer. To me, the basic reality is that when the engine decides it is time to check *any* "run" queue, micro or macro or whatever, it is at that point done with any user code that was previously executing. It will pull a function off the queue and run it, and that won't happen again until that function finishes its synchronous execution. Call it what you want I guess, "tick", "loop", "cycle", whatever. – Pointy Jun 22 '23 at 12:58
  • @Pointy, yes that is actually also how I think of it, but here we are with all this documentation ;-) – trincot Jun 22 '23 at 12:59
  • Also `process.nextTick()` has always seemed essentially like a "mini-microtask queue". – Pointy Jun 22 '23 at 12:59
  • If we say that tick is event loop iteration then `then`-callbacks (or their `await` function-resuming counterparts) will be executed to the same tick? – AlexBor Jun 22 '23 at 13:25
  • 1
    Yes, of course assuming that the promises on which they were registered are in a resolved state. – trincot Jun 22 '23 at 13:28
  • And browsers have a `nextTick` it's called `queueMicrotask` – Keith Jun 22 '23 at 16:13
  • @Keith, it is not exactly the same. `queueMicrotask` uses the same queue as promise resolutions, while `nextTick` is a queue that is separate from that, has a higher priority, and is specific to Node. – trincot Jun 22 '23 at 16:44
  • @trincot Yeah, not exactly the same, but I'm more comparing it to browser & node, I don't believe browsers have anything higher up chain here. But yeah, if your node only then there is a difference. – Keith Jun 22 '23 at 16:48
  • `queueMicrotask` has same behaviour as `Promise.resolve().then`, except that it doesn't return a promise object. – trincot Jun 22 '23 at 16:53
0

When JS encounters await it executes the statement immediately and if the statement returns a fullfilled promise or a value (which is treated as a resolved promise) creates a microtask pushing it to the end of the current task for executing the next statements.

We could refactor the code to replace console.log with a fullfilled promise equivalent and console.log it:

async function foo(name) {
    console.log(name, "start");
    const promise = Promise.resolve(console.log(name, "middle"));
    console.log(promise);
    await promise;
    console.log(name, "end"); // executed in 2 separate microtasks
}

foo("First");
foo("Second");

enter image description here

Interestingly enough but we can use microtasks to our advantage for example collecting sync input from the user and react to it in a microtask. Here I use that to collect mouse pointer data in any order and handle it later in a microtask.

That's a really cool feature, it makes the UI looking sync, because there's no new tasks created. On the other hand using setTimeout could give a less pleasant result with possible jittering of the UI since new tasks are created allowing the event loop fire any intermediate tasks from the UI/IO events that could even mutate our state used in our current UI work:

    // use a resolved Promise to postpone the move as a microtask so
    // the order of state mutation isn't important
    ops[prop] && Promise.resolve().then(ops[prop]);

Is there a design pattern for logic performed on multiple event listeners

Alexander Nenashev
  • 8,775
  • 2
  • 6
  • 17