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

Utilize JavaScript to send login information to a website

I have a specific task in mind: creating a script that will automatically fill in certain data on an HTML website. To illustrate, let's imagine the site is the StackOverflow login page and I want to input my username and password. For the username fi ...

What is the method by which the asynchronous function produces the ultimate output?

Is there a way to modify a Dojo framework-created class with an asynchronous method so that it only returns the final value instead of a Promise or any other type? ...

"Resolving the issue of Django request.FILE returning a null value

HTML Template: <form method="post" action="" enctype="multipart/form-data" class="form-group"> {% csrf_token %} <h1 class="m-3 text-center">New Post</h1> <div id="tui-image-e ...

React.js encountered an error: Objects cannot be used as a React child (found: object containing keys {type, data})

My current project involves displaying MySQL data in a table using Node.js and React.js. However, I keep encountering the following error: Error: Objects are not valid as a React child (found: object with keys {type, data}). If you meant to render a colle ...

Integrate PEM certificate into an Http Request with AngularJS/NodeJS

Currently, I am working on an application that requires retrieving data from a REST endpoint within an encrypted network. Accessing this data is only possible by physically being present (which is currently not an option) or using a PEM certificate provide ...

Access the $event object from an Angular template selector

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script> <input type="file" #myFile multiple /> <button (click)="onDelete(myFile.event)">DeleteFiles</button> My code snippet is experienci ...

Tips for retrieving multiple data outputs from an ajax success function

Within my project, I have two separate JavaScript files named myJs1.js and myJs2.js. One of the methods in myJs1.js is invoking a method from myJs2.js. My goal is to retrieve the values r1 and r2 into the results (within myJs1.js). I attempted to achiev ...

Variables for NPM Configuration

After researching the best way to securely store sensitive information, I decided to utilize the config package and environment variables for added security. Here is how I implemented this setup: Created a config directory containing two files: default.js ...

The combination of Firebase Storage's listAll() method and getDownloadURL() function is not functioning properly

I created a function in my utils file that is designed to find and retrieve the URLs of images stored within a specific folder on Firebase Storage. The folder path is "proj_name/screenshots/uid/" and it contains 8 images. I have imported this function into ...

What is the best way to pass props down to grandchildren components in React?

I'm trying to pass some props from a layout to its children. The issue is, it works fine when the child component is directly nested inside the layout, but it doesn't work when the child component is wrapped in another element like a div. Exampl ...

Whenever a query is entered, each letter creates a new individual page. What measures can be taken to avoid this?

Currently, I am working on a project that involves creating a search engine. However, I have encountered an issue where each time a user types a query, a new page is generated for every alphabet entered. For instance, typing 'fos' generates 3 pag ...

Display only one field and hide the other field using a jQuery IF/ELSE statement

Hey there! I have a situation where I need to toggle between two fields - one is a text field and the other is a text_area field. When a user clicks on one field, the other should be hidden and vice versa. I've tried using JQuery for this: $(document ...

Utilize time in submitting a form

Is it possible to schedule a form submission at a specific time, such as 12:00? I have already created a countdown timer and now would like the form to be submitted automatically at a certain time. <html> <head> </head> <body> ...

Getting Session from Next-Auth in API Route: A Step-by-Step Guide

When printing my session from Next Auth in a component like this, I can easily see all of its data. const session = useSession(); // ... <p>{JSON.stringify(session)}</p> I am facing an issue where I need to access the content of the session i ...

Exploring the isolate scope within a compiled directive

I successfully managed to compile a directive using this piece of code: let element, $injector, $compile, link, scope; element = angular.element(document.getElementById(#whatever)); $injector = element.injector(); $compile = $injector.get('$compile& ...

Ways to display a specific HTML element or tag depending on the given prop in VueJs

When working with Vue.js, it's important to remember that a <template> can only have one root element. But what should be done if you need to render different html elements based on a prop? For instance, let's say we have a <heading> ...

Encountering a CORS issue while attempting to make a GET request to an API in an

Looking to retrieve HTML data from a website using an API. The target URL is: https://www.linkedin.com/ Trying to fetch the HTML page as text from this specific URL. Here's what I attempted: getData() { const api = "https://www.linkedin. ...

Express: utilizing rawBody or buffer for a specific endpoint

I am looking to access the rawBody (buffer) specifically for a POST request on one route within my application. Currently, I have the following code snippet in my app.js: app.use(bodyParser.json({ verify: function(req, res, buf) { req.rawBody = buf; }})) ...

Guide to adding customized CSS and JavaScript to a specific CMS page on Magento

I'm trying to incorporate a lightbox for video playback on a specific page of my CMS. I've placed my CSS file in JS/MY THEME/JQUERY/PLUGIN/VENOBOX/CSS/myfile.css and the JS files in JS/MY THEME/jquery/plugins/venobox/js/myfile.js, but it doesn&ap ...

What is the method utilized by Redux to store data?

I am currently developing a small application to enhance my understanding of how to utilize redux. Based on my research, redux allows you to store and update data within the store. In my application, I have implemented an HTML form with two text inputs. Up ...