Attempting to grasp the sequence in which setTimeout is ordered alongside Promise awaits

I've been puzzling over the sequence of events in this code. Initially, I thought that after a click event triggered and Promise 2 was awaited, the for loop would resume execution once Promise 1 had been resolved - however, it turns out the outcome is not entirely predictable. How should I approach understanding the ordering of these events within the event loop?

let zeros = new Array(10000).fill(0);

(async () => {

    let stop = false;
    document.addEventListener('click', async() => {
        console.log('click');
        stop = true;
        await new Promise((r) => setTimeout(r)); //2
        stop = false;
    });

    for (let zero of zeros) {
        await new Promise((r) => setTimeout(r)); //1
        if (stop) {
            break;
        }
        console.log(zero);
    }
})();
click here

Answer №1

JavaScript operates on a single thread. It maintains a queue of macrotasks (such as setTimeout callbacks and click handlers) and microtasks (for handling async functions/promises). Microtasks always take precedence over macrotasks.

In the scenario described, the execution of the click handler's macrotask is dependent on line //1 suspending due to a setTimeout call.

However, the click handler then sets stop=true and initiates another setTimeout, leading to its own suspension.

This creates a race condition. Which setTimeout will complete first: the one at line //1 or line //2?

If both setTimeout calls have the same delay (or unspecified), they are expected to execute in the order they were called. Yet, browser behavior with setTimeout can be unpredictable.

By specifying a 100 ms delay in the line //2 setTimeout call, you increase the likelihood of breaking the loop after a click event. However, even this is not foolproof.

Browsers independently decide on setTimeout delays without guaranteeing exact timing.

The impact of differing delays becomes apparent when clicking rapidly versus slowly. Chrome may exhibit varying results based on click speed rather than frequency. The inconsistency stems from how browsers manage tasks differently.

Unpredictable setTimeout delays are attributable to various factors detailed here. Notably,

The timeout could be delayed if the page (or OS/browser) prioritizes other tasks

Rapid clicks trigger the browser to apply erratic additional delays to setTimeouts, altering their execution sequence. The browser likely prioritizes UI-related setTimeouts for enhanced responsiveness, potentially impacting the loop outcome. Consequently, rapid clicking leads to shuffled setTimeout executions.

Answer №2

One possible explanation for this behavior is the 4ms minimum timeout mentioned by Andrew: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified

According to the HTML standard, browsers are required to enforce a minimum timeout of 4 milliseconds once a nested call to setTimeout has been queued 5 times.

await wraps the code below in a callback function. In the case of a for loop, the remaining iterations are executed within that callback, meeting the conditions of a "nested" setTimeout.

If you change console.log(zero); to

console.log(new Date().getMilliseconds());
, you will notice that the first 5 logs occur very close together, while the subsequent ones are further apart.

Here's what's happening step by step:

First, we set up an almost infinite loop:

await new Promise((r1)=>setTimeout(r1)); //1
if (stop) {
   break;
}

(r)=>setTimeout(r) is immediately executed, causing r1() to be pushed to the end of the event queue. After 5 iterations, a timestamp 4ms into the future is attached to this action. When the promise resolves, await bundles the following code into a callback which also gets queued at the end of the event queue.

Upon a click:

stop = true;
await new Promise((r2)=>setTimeout(r2)); //2
stop = false

stop = true and (r2)=>setTimeout(r2) are executed right away, pushing r2() to the end of the event queue. Again, await bundles the following code into a callback queued after the promise resolves.

There are six events taking place:

  1. r1 with a 4ms timeout is added to the end of the event queue
  2. r1 reaches the top of the event queue. If 4ms have passed, r1 is executed, and if (stop) break; is moved to the bottom of the event loop. If not, r1 is pushed back to the end again.
  3. if (stop) break; is executed when it reaches the top of the event queue. If the loop isn't broken, event 1 is replayed.
  4. User clicks, triggering stop = true and adding r2 to the end of the event queue.
  5. r2 is executed when it reaches the top of the event queue, followed by stop = false being queued.
  6. stop = false is then executed.

A key point is that event 3 needs to happen between events 4 and 6 for the loop to break. With immediate actions and a 4ms delay, there is a specific order of events where the loop won't break on a click:

  • event 1 (r1 queued with 4ms delay)
  • event 2 (4ms elapse, if (stop) break; queued)
  • event 3 & 1 (if (stop) break; executed, loop remains unbroken, r1 queued w/ 4ms delay)
  • event 4 (user click, stop = true, r2 queued)
  • event 2 (4ms don't pass, r1 queued again)
  • event 5 (stop = false queued)
  • event 2 (4ms elapse, if (stop) break; queued)
  • event 6 (stop = false executed)
  • event 3 & 1 (if (stop) break; executed, loop remains unbroken, r1 queued w/ 4ms delay)

This scenario creates a race condition in a single-threaded environment.


To mitigate this issue, synchronizing the two timeouts with intervals greater than 4ms may help resolve the problem

let zeros = new Array(10000).fill(0);

(async () => {
    let stop = false;
    document.addEventListener('click', async ()=>{
        console.log('click');
        stop = true;
        await new Promise((r)=>setTimeout(r,5)); //2
        stop = false;
    });

    for (let zero of zeros) {
        await new Promise((r)=>setTimeout(r,5)); //1
        if (stop) {
            break;
        }
        console.log(zero);
    }
})();


Nevertheless, there is still a theoretical possibility for the loop to continue uninterrupted. The following sequence of events could allow the loop to proceed:

  • event 1 (r1 queued w/ 5ms delay)
  • event 4 (user click, stop = true, r2 queued w/ 5ms delay)
  • event 2 (5ms haven't passed from event 1, r1 queued again)

It's plausible that events 1 and 4 occurred rapidly, but events 2 and 5 had some delay between them - this enables event 5 to complete before event 2.

  • event 5 (5ms elapsed since event 4, stop = false queued)
  • event 2 (5ms elapsed since event 1, if (stop) break; queued)
  • event 6 (stop = false executed)
  • event 3 & 1 (if (stop) break; executed, loop remains unbroken, r1 queued)

Although I have not managed to reproduce this behavior, it's advisable to avoid coding in such a way.


To achieve your desired outcome, consider using setInterval instead of a loop. This approach cycles the callback to the end of the queue after execution, allowing other events to interject. Unlike while and for loops which block additional execution while running.

let stop = false;

document.addEventListener('click', () => (stop = true));

const id = setInterval(() => {
  console.log('running');
  if (stop) {
    console.log('stopped');
    clearInterval(id);
  }
});

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Surprising outcomes when using Mongoose

Questioning Unusual Behavior There is a model in question: //test.js var mongoose = require('../utils/mongoose'); var schema1 = new mongoose.Schema({ name: String }) var schema2 = new mongoose.Schema({ objectsArray: [schema1] }); schema1.pre( ...

Utilizing the Google Geocode API to handle a promise with a substantial array

My Objective To efficiently process a large array using the .map method and interact with the Google Geocoder API through promises to get location data. The goal is to utilize Promise.all to store results in a .json file upon completion of the operation. ...

What is preventing me from being able to import a React component from a file?

I've double-checked my code and everything seems correct, but when I view it on the port, only the app.js file is displayed. import React from 'react'; import ImgSlider from './ImgSlider'; import './App.css'; function ...

There are critical vulnerabilities in preact-cli, and trying to run npm audit fix leads to a never-ending loop between versions 3.0.5 and 2.2.1

Currently in the process of setting up a preact project using preact-cli: npx --version # 7.4.0 npx preact-cli create typescript frontend Upon completion, the following information is provided: ... added 1947 packages, and audited 1948 packages in 31s 12 ...

Duplicate text content from a mirrored textarea and save to clipboard

I came across some code snippets here that are perfect for a tool I'm currently developing. The codes help in copying the value of the previous textarea to the clipboard, but it doesn't work as expected when dealing with cloned textareas. Any sug ...

Is there a way to identify a location in close proximity to another location?

With a position at (9,-3), I am looking to display all positions surrounding it within a square red boundary. However, I am struggling to find the algorithm to accomplish this task. Any help or alternative solutions would be greatly appreciated. Thank you ...

JavaScript Function Not Executed in Bottom Section

I've implemented AngularJS includes in my project. Below is the code snippet from my index.html: <!DOCTYPE html> <html ng-app=""> <head> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"> ...

Data binding in Vue.js seems to be malfunctioning

I'm having trouble with my Vue component creation. I've been using v-show to hide certain elements, but it doesn't seem to be working as intended. The goal is to hide an element when clicked from a list by setting element.visible to false i ...

Using Selenium WebDriver to Extract Variables from JavaScript SourceCode

Currently, I am dealing with a web page source code that utilizes JavaScript to display text. Below is an excerpt from the source: var display_session = get_cookie("LastMRH_Session"); if(null != display_session) { document.getElementById("sessionDIV") ...

Reloading a Nuxt.js page triggers the fetch function

I'm facing an issue with nuxt.js fetch(). Whenever I reload the page, it does not fetch the data again. It only fetches if I come from a router link. How can I force it to actually refetch the data from my API? export default { async fetch() { ...

The Vue DevTools are functioning as expected, but there seems to be an issue

Encountering a peculiar issue where the value displayed in Vue DevTools is accurate, matching what is expected in my data. When I first click on the "Edit" button for an item, the correct value appears in the browser window as intended. However, upon clic ...

Storing complex data structures in Firebase using VUEX

I am struggling to properly store my 'players' data within my team data structure. Currently, it is being saved with the team id but I need it to be nested inside of teams. Essentially, I want the players to seamlessly integrate and be appended ...

Implementing the onClick function for the correct functionality in a ReactJS map component

I have designed a mockup and now I am trying to bring it to life by creating a real component. View my component mockup here Starting with something simple, I created 3 buttons and an array. However, when I click on any button, all features are displayed ...

Exploring the differences between utilizing Node JS Express for server-side development and displaying console output to

Recently, I started delving into the world of node js and came across IBM's speech to text sample application (https://github.com/watson-developer-cloud/speech-to-text-nodejs). This app utilizes the express framework and showcases transcribed audio fr ...

Unexpected output from nested loop implementation

Having some arrays, I am now trying to iterate through all tab names and exclude the values present in the exclusion list. json1 ={ "sku Brand": "abc", "strngth": "ALL", "area ...

What are the benefits of pairing Observables with async/await for asynchronous operations?

Utilizing Angular 2 common HTTP that returns an Observable presents a challenge with nested Observable calls causing code complexity: this.serviceA.get().subscribe((res1: any) => { this.serviceB.get(res1).subscribe((res2: any) => { this.se ...

Managing a prolonged press event in a React web application

Hello everyone! I am currently using React along with the Material UI library. I have a requirement to handle click events and long-press events separately. I suspect that the issue might be related to asynchronous state setting, but as of now, I am unsu ...

Moving the input box down to the lower portion of the screen

My goal is to create an interactive input box that glides smoothly to the bottom of the screen when clicked by the user. However, the current implementation causes the input box to move too far down, requiring the user to scroll down in order to see it. H ...

What is the best way to execute code after an element has been included in ngFor in Angular 2+?

Within my component, there is a function that adds an element to an array displayed using *ngFor in the view. After adding the element to the array, I want to target it by ID and scroll to it. However, the issue is that this code runs before the element a ...

Using Node.js and Express to import a simple JavaScript file as a router

Can anyone help me understand how to import the user.json json file into my user.js? I want the json file to be displayed when typing /user but I'm struggling with the new version of Node. index.js import express from 'express'; import body ...