January 28

Event Loop. Myths and reality

There are numerous publications on the web about the Event Loop and how it works, and new articles continue to appear on popular resources to this day. Unfortunately, not all the information provided in these materials is verified or reliable. As a result, the concept itself has become surrounded by a number of myths and speculations. Sometimes, even experienced developers require a great deal of attention and experience to discern the truth from pure fiction.

In this article, let's attempt to determine where the truth lies. If you believe there are errors, inaccuracies, or omissions in the article, please leave your comments, and we will endeavor to address all doubts and queries together.

So, let's begin!

Table of contents

Does the term "Event Loop" even exist in JavaScript?

Yes, the term Event Loop does exist. However, it cannot be found in the ECMA-262 specification. The Event Loop is not part of the JavaScript (ECMAScript) language and, therefore, is not regulated by its specification. This term exists in the realm of the HOST executor, and a specific implementation of the JavaScript engine can utilize the Event Loop at its discretion as an API of the environment in which it is executed.

What is the official source of information for the Event Loop?

As we have mentioned earlier, there is no mention of the term Event Loop in the ECMA-262 specification since it falls outside the language's scope but within the domain of the HOST executor, responsible for executing JavaScript code. Consequently, information about the Event Loop should be sought from sources that govern or document this implementing environment. There are numerous such environments today, which can be broadly classified into browser-based and non-browser-based.

Browser-based environments

The mechanisms of such environments are governed by the WHATWG organization through the HTML specification.

Specifically, the Event Loop is addressed in section 8.1.7 Event loops of the specification. We will discuss the algorithms and standards of the Event Loop in the Web API later on. For now, it is worth mentioning that browsers typically rely on the API of the operating system in which they are executed; for example, Chromium in macOS relies on NSRunLoop, and in Linux, it relies on glib.

The exception here is Electron, which, due to its purported cross-platform nature, encountered challenges in implementing the Event Loop for different operating systems and consequently transitioned to using the libuv library, akin to Node.js (more on that later).

Non-browser-base environments

In non-browser-based environments, as the name suggests, the HTML standard is not implemented. Since there are no international standards and specifications other than ECMA-262 on how such environments should operate, their own documentation is the only official source of information.

The most common non-browser environment by far is Node.js.

In the Node.js documentation (as of the time of writing, the Node.js version is v21.6.1), there is a section on the libuv even loop, which describes the only function available in the Node API, napi_get_uv_event_loop, designed to retrieve a reference to the current Event Loop.

Unfortunately, this documentation does not provide any other description of the Event Loop. However, it is evident that to ensure its operation, Node.js utilizes the libuv library, which was specifically developed for Node.js to provide the implementation of the Event Loop in this environment. The library is now also used by some other projects. The internal documentation of the library contains the uv_loop_t API section, which offers a formal API specification of the Event Loop. Additionally, the documentation contains a schematic diagram of the Event loop and a guide for working with the Event Loop within this library.

Other non-browser environments, such as Deno and Bun, also rely on the libuv library to work with the Event Loop. Mobile environments, including React Native, NativeScript and Apache Cordova, are also non-browser-based. They rely on the API of the corresponding operating system in which they are executed. For instance, for Android, this is Android.os.Looper, and for iOS, it is RunLoop.

But without the Event Loop mechanism, the execution of JavaScript code becomes extremely difficult to imagine. How could the ECMA-262 specification overlook such an important aspect?

While the ECMA-262 specification does not include the term Event Loop, this does not mean that it does not regulate the process of code execution in any way. However, this regulation is not concentrated under one specific concept. In general, the entire section 9 Executable Code and Execution Contexts, is devoted to the JavaScript execution process. Within this section, clause 9.7 Agents introduces the term Agent and provides the structure of the Agent Record, which includes fields responsible for blocking this agent. The implementation of the agent remains on the responsibility of the HOST executor, but with some limitations. Specifically, section 9.9 Forward Progress, outlines the basic requirements for agent implementation:

These restrictions, combined with the guarantees of section 29 Memory Model, are sufficient for all SEQ-CST records to eventually be observable for all agents.

Can the description of the Event loop in the HTML specification be considered a reference?

Since the official Event Loop specification exists only within the HTML standard, there are quite a lot of non-browser variants. These are all developed at the discretion of the developers and each has its own characteristics. A separate article will be required for each variant (some are already available online). In addition, many implementations, in one way or another, rely on the HTML specification, to prevent reinventing the wheel, which is logical.

Whether to consider the HTML specification as a reference in the Event Loop part is a moot point. There is no definite answer to this, but, given the above, for further consideration of the issue, from now on, we will operate with this specification.

Usually, when describing the Event loop, people tend to only talk about synchronous and asynchronous JavaScript operations.

As we mentioned earlier, the Event Loop does not lie within the JavaScript language field. For JavaScript, it is a kind of external mechanism ("service", if you like) that allows you to organize your work. At the same time, the Event Loop itself is not limited only to the execution of the JS code. The Event Loop is responsible for many processes, such as input/output operations (mouse and keyboard events, file reading and writing, etc.), event coordination, rendering, network operations, and much more.

Is the Event loop thread-safe?

This question is quite interesting. Earlier, we discussed section 9.9 Forward Progress of the ECMA-262 specification, which places certain limitations on the agent's implementation. This section does not explicitly indicate thread safety. On the contrary, it states that if there are multiple agents in the same thread, only one of them should progress. This model clearly indicates that there is no need for thread safety, as only one agent can work at a time.

In most cases, it is. For example, the libuv library used in Node.js explicitly states that their implementation is not thread-safe, and their Event Loop should be used single-threaded or independently organize work in multithreaded mode.

However, with browser implementations, everything is not so straightforward.

Firstly, it's worth clarifying that in section 8.1.2 Agents and agent clusters, the HTML specification identifies several types of agents:

Depending on the type of agent, the specification identifies three types of Event loop

  • window event loop - for similar-origin window agent
  • worker event loop - for dedicated worker agent, shared worker agent and service worker agent
  • worklet event loop - for worklet agent

Among these, worker event loop and worklet event loop have the agent flag [[CanBlock]] set to true, obliging them to follow the restrictions of 9.9 Forward Progress. Therefore, such Event Loops will each work in their own dedicated thread.

On the other hand, it is possible to use multiple window event loops at once, simultaneously in one thread (for example, several browser tabs can share one thread if the browser developer so desires).

It is often claimed that an event loop consists of macrotasks and microtasks, but is this true?

Not quite. The term "macrotask" does not actually exist in the specification. In reality, the Event Loop is comprised of a task queue and a microtask queue, and their mechanics are fundamentally different.

It's worth noting that, contrary to its name, the task queue is not technically a queue; it is actually a set. In contrast, the microtask queue is indeed a queue. This leads to an important distinction: at the start of the next iteration, the task queue may contain numerous tasks in different states. Traditionally, the queue algorithm presumes that the first element is removed from the queue (dequeue). However, with the task queue, the first element may not necessarily be a runnable task at that specific moment. Instead of a straightforward dequeue operation, the process has to locate the first task in the runnable task status and extract it from the set. This deviation from the standard queue algorithm cannot be considered a true implementation of the queue algorithm. On the other hand, microtasks are placed into the queue and are removed in the order in which they were added. A more in-depth look into this process is detailed below.

What goes into the task queue?

Cognitive dissonance often arises in this matter. On one hand, the task queue is used for deferred task processing, i.e., asynchronous execution. But then what happens to the synchronous code? To figure this out, it's worth delving a little beyond JavaScript (since we already know that the Event Loop operates beyond it) and realizing that for the browser, JS code itself is just one of the many entities it works with. By parsing the script file or the <script> tag, the browser receives a tokenized result. The completion of this tokenized result itself becomes a separate task, for which a task of a global task type is generated in the Event Loop. Thus, in fact, the synchronous code is already inside the Event Loop in the form of a task from the very beginning of its execution.

Furthermore, as the script is executed, new tasks are generated and placed in the same Event Loop.

What else goes into the task queue?

  • Events - dispatching an Event object at a particular EventTarget object is often done by a dedicated task, but not always. There are many events that are dispatched in other tasks. For example, the MouseEvent and KeyboardEvent events can be combined into one task, the source of which is associated with the user interaction task source
  • Parsing - the HTML parser tokenizing one or more bytes, and then processing any resulting tokens, is typically a task
  • Callbacks - as a rule, they fall into the task queue, the same applies to the classic example of setTimeout(() => {}), in this case, a callback is passed to setTimeout, which will become a separate task in the task queue
  • Using a resource - in the case of non-blocking resource fetching (for example, an image), a separate task is set in the task queue
  • Reacting to DOM manipulation - some elements generate a task in the task queue in response to DOM manipulation. As an example, inserting a new element into the DOM will trigger the task of re-rendering the parent element

What is the purpose of the microtask queue when there is already a task queue, and what tasks enter it?

In the case of the task queue, the responsibility for defining and adding tasks to the queue lies with the agent. The microtask queue is an option available to the task at runtime through the Web API, enabling the task to address its own asynchronous needs. Technically, every task, whether executing JavaScript code, tokenizing HTML text, handling I/O events, or other operations, can use the microtask queue to achieve its objectives.

Looking specifically at JavaScript, the language assumes the presence of its own asynchronous operations that are not covered by the HTML specification. These operations are best exemplified by Promises. To be more specific, section Promise Resolve Functions of the specification describes the process of resolving a promise. Step 15 involves performing a HostEnqueuePromiseJob, the implementation of which resides with the HOST executor, but adheres to certain requirements, such as running jobs in the same order in which the HostEnqueuePromiseJobs that scheduled them were called.

As previously mentioned, the implementation of the HostEnqueuePromiseJobs mechanism is entirely the responsibility of the HOST executor. However, using the microtask queue in this context seems highly sensible. For further clarification, let's refer to the source code of one of the most widely used JavaScript engines - V8 (at the time of writing, the latest version of the engine is 12.3.55).


// https://tc39.es/ecma262/#sec-promise-resolve-functions
// static
MaybeHandle<Object> JSPromise::Resolve(Handle<JSPromise> promise,
                                       Handle<Object> resolution) {

  // 13. Let job be NewPromiseResolveThenableJob(promise, resolution,
  //                                             thenAction).
  Handle<NativeContext> then_context;
  if (!JSReceiver::GetContextForMicrotask(Handle<JSReceiver>::cast(then_action))
           .ToHandle(&then_context)) {
    then_context = isolate->native_context();

  Handle<PromiseResolveThenableJobTask> task =
          promise, Handle<JSReceiver>::cast(resolution),
          Handle<JSReceiver>::cast(then_action), then_context);
  if (isolate->debug()->is_active() && IsJSPromise(*resolution)) {
    // Mark the dependency of the new {promise} on the {resolution}.
    Object::SetProperty(isolate, resolution,
  MicrotaskQueue* microtask_queue = then_context->microtask_queue();
  if (microtask_queue) microtask_queue->EnqueueMicrotask(*task);
  // 15. Return undefined.
  return isolate->factory()->undefined_value();

In the V8 implementation, we can observe that in order to execute HostEnqueuePromiseJob, the engine places the corresponding microtask into the microtask queue, thus confirming our assumption

How exactly does the Event Loop algorithm function? Is there a sample implementation available?

As mentioned above, there are many variants for implementing the algorithm. Browser-based environments are guided by the HTML specification, and for the implementation of the mechanism, as a rule, they rely on the API of the OS in which they are executed. In this case, it is worth looking for implementation examples in the source codes of each specific browser engine, as well as in the corresponding libraries and the API OS. In platforms using third-party libraries such as libuv, an example of an implementation is worth looking for in this library itself (libuv is open source). However, it should be understood that each implementation is independent and may differ greatly from others.

As an illustration and in order not to rely on any specific implementation, let's try to depict our pseudo-version of the mechanism. Since we are talking in the context of JavaScript, for ease of understanding and perception, we will implement it in Typescript.

The following listing shows the interfaces necessary for the operation of the Event loop. These interfaces are left solely for demonstration purposes in accordance with the HTML specification and do not reflect the internal structures of any real HOST executor in any way. The Event loop algorithm itself will be given below.

As mentioned earlier, there are several variations in implementing this algorithm. Browser-based environments adhere to the HTML specification, and typically rely on the OS API for the actual implementation. In this regard, it is advisable to seek implementation examples in the source code of each specific browser engine, as well as in the corresponding libraries and OS API. In the case of platforms using third-party libraries, such as libuv, it is beneficial to examine the implementation example within the library itself (libuv being open-source). However, it is important to note that each implementation is independent and may significantly differ from others.

For illustrative purposes, and in an attempt not to be reliant on any specific implementation, let's consider a pseudo-version of the mechanism. Given that we are discussing this within the context of JavaScript, for the sake of comprehension and clarity, we will implement this in TypeScript.

The following listing displays the necessary interfaces for the Event Loop operation. It should be noted that these interfaces are provided purely as a demonstration, aligned with the HTML specification, and do not reflect the internal structures of any real HOST executor.

* A browsing context is a programmatic representation of a series of documents, multiple of which can live within a
* single navigable. Each browsing context has a corresponding WindowProxy object, as well as the following:
* - An `opener browsing context`, a browsing context or null, initially null.
* - An `opener origin` at creation, an origin or null, initially null.
* - An `is popup` boolean, initially false.
* - An `is auxiliary` boolean, initially false.
* - An `initial UR`L, a URL or null, initially null.
* - A `virtual browsing context group ID` integer, initially 0. This is used by cross-origin opener policy reporting,
*   to keep track of the browsing context group switches that would have happened if the report-only policy had been
*   enforced.
* A browsing context's active window is its WindowProxy object's [[Window]] internal slot value. A browsing context's
* active document is its active window's associated Document.
* A browsing context's top-level traversable is its active document's node navigable's top-level traversable.
* A browsing context whose is auxiliary is true is known as an auxiliary browsing context. Auxiliary browsing contexts
* are always top-level browsing contexts.
* Note: For a demonstration purposes and for simplicity the BrowserContext is reflecting the Window interface which is
*       not fully correct, as the might be different implementations of the BrowserContext.
interface BrowsingContext extends  Window {}
 * A navigation request is a request whose destination is "document", "embed", "frame", "iframe", or "object"  *
 * Note: For a demonstration purposes and for simplicity the NavigationRequest is reflecting the Window interface
 *       which is not correct as the NavigationRequest is a different structure mostly use for
 *       `Handle Fetch` (https://w3c.github.io/ServiceWorker/#handle-fetch)
interface NavigationRequest extends Window {}
interface Environment {
  id: string;
  creationURL: URL;
  topLevelCreationURL: URL;
  topLevelOrigin: string | null;
  targetBrowsingContext: BrowsingContext | NavigationRequest | null;
  activeServiceWorker: ServiceWorker | null;
  executionReady: boolean;

interface EnvironmentSettingsObjects extends Environment {
  realmExecutionContext: ExecutionContext;
  moduleMap: ModuleMap;
  apiBaseURL: URL;
  origin: string;
  policyContainer: PolicyContainer;
  crossOriginIsolatedCapability: boolean;
  timeOrigin: number;

interface Task {
  // A series of steps specifying the work to be done by the task.
  // will be defined in a certain Task implementation
  steps: Steps;
  // One of the task sources, used to group and serialize related tasks.
  // Per its source field, each task is defined as coming from a specific task source. For each event loop, every
  // task source must be associated with a specific task queue.
  // Essentially, task sources are used within standards to separate logically-different types of tasks,
  // which a user agent might wish to distinguish between. Task queues are used by user agents to coalesce task sources
  // within a given event loop.
  source: unknown;
  // A Document associated with the task, or null for tasks that are not in a window event loop.
  // A task is runnable if its document is either null or fully active.
  document: Document | null;
  // A set of environment settings objects used for tracking script evaluation during the task.
  environmentSettingsObject: Set<EnvironmentSettingsObjects>;

interface GlobalTask extends Task {
  steps: Steps; // redefine/implement steps for this particular task type

interface EventLoop {
  taskQueue: Set<Task>;
  // Each event loop has a microtask queue, which is a queue of microtasks, initially empty. A microtask is a colloquial
  // way of referring to a task that was created via the queue a microtask algorithm.
  // For microtaskQueue is used just to illustrate that the specification supposes it to be a logical queue, rather
  // than a set of tasks. From technical perspective, a real implementation might use a `Set` like for taskQueue, or
  // any other structure at the discretion of the agent's developer.
  microtaskQueue: Array<Task>;
  // Each event loop has a currently running task, which is either a task or null. Initially, this is null. It is used
  // to handle reentrancy.
  currentRunningTask: Task | null;
  // Each event loop has a performing a microtask checkpoint boolean, which is initially false. It is used to prevent
  // reentrant invocation of the perform a microtask checkpoint algorithm.
  performingAMicrotaskCheckpoint: boolean;

interface WindowEventLoop extends EventLoop {
  // Each window event loop has a DOMHighResTimeStamp last render opportunity time, initially set to zero.
  lastRenderOpportunityTime: number;
  // Each window event loop has a DOMHighResTimeStamp last idle period start time, initially set to zero.
  lastIdlePeriodStartTime: number;

/** Just for demonstration purposes. Such a helper not necessarily should be presented in the real implementation */
function isWindowEventLoop(eventLoop: EventLoop): eventLoop is WindowEventLoop {
  return 'lastRenderOpportunityTime' in eventLoop && 'lastIdlePeriodStartTime' in eventLoop;

We have described the necessary interfaces, at least the part that we need to understand the algorithm below. Now, let's look at the Event Loop algorithm itself. It is important to clarify that the algorithm is an illustration of section Processing model specification and does not reflect the actual implementation on the HOST executor in any way.

 * Processing the event loop according to the ` Processing model`
 * https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
function runEventLoop(eventLoop: EventLoop) {
  // 1. Let oldestTask and taskStartTime be null.
  let oldestTask: Task | null = null;
  let taskStartTime: number | null = null;
  while (true) {
    // 2. check if the taskQueue has a runnable task and if there is one
    //   2.1. Let taskQueue be one such task queue, chosen in an implementation-defined manner.
    //   2.2. ... will be done below
    //   2.3. Set oldestTask to the first runnable task in taskQueue, and remove it from taskQueue.
    oldestTask = getFirstRunnableTaskFromQueueAndRemove(eventLoop.taskQueue);
    if (oldestTask !== null) {
      // 2.2. Set taskStartTime to the unsafe shared current time.
      taskStartTime = Date.now();
      // 2.4. Set the event loop's currently running task to oldestTask.
      eventLoop.currentRunningTask = oldestTask;
      // 2.5. Perform oldestTask's steps.
      // 2.6. Set the event loop's currently running task back to null.
      eventLoop.currentRunningTask = null;
      // 2.7. Perform a microtask checkpoint.
    // 3. Let hasARenderingOpportunity be false.
    let hasARenderingOpportunity = false;
    // 4. Let `now` be the unsafe shared current time.
    let now = Date.now();
    // 5. If oldestTask is not null, then:
    if (oldestTask !== null) {
      // 5.1. Let top-level browsing contexts be an empty set.
      const topLevelBrowsingContexts = new Set();
      // 5.2. For each environment settings object settings of oldestTask's script evaluation
      //      environment settings object set:
      oldestTask.environmentSettingsObject.forEach((settingsObject) => {
      // 5.2.1. Let `global` be settings's global object.
      const global = settingsObject.targetBrowsingContext;
      // 5.2.2. If `global` is not a Window object, then continue.
      if (!(global instanceof Window)) {
      // 5.2.3. If global's browsing context is null, then continue.
      if (!global.document) {
      // 5.2.4. Let tlbc be global's browsing context's top-level browsing context.
      const tlbc = global.document;
      // 5.2.5. If tlbc is not null, then append it to top-level browsing contexts.
      if (tlbc !== null) {
    // 5.3. Report long tasks, passing in taskStartTime, now (the end time of the task), top-level browsing contexts,
    //      and oldestTask.
    //      https://w3c.github.io/longtasks/#report-long-tasks
    // ...
  // 6. if this is a window event loop, then: Update the rendering
  if (isWindowEventLoop(eventLoop)) {
  // 7. If all of the following are true:
  //   - this is a window event loop;
  //   - there is no task in this event loop's task queues whose document is fully active;
  //   - this event loop's microtask queue is empty; and
  //   - hasARenderingOpportunity is false,
  // then:
  //   ...run computeDeadline and hasPendingRenders steps for WindowEventLoop
  // 8. If this is a WorkerEventLoop, then:
  //   ...run animation frame callbacks and update the rendering of that dedicated worker

According to the specification, some of the operations can be transferred to separate functions and algorithms. For example, step 2.7 Perform a microtask checkpoint has been transferred to a separate function called performMicrotaskCheckpoint.

/** Finds and returns the first runnable task in the queue. The found Task will be removed from the queue */
function getFirstRunnableTaskFromQueueAndRemove(taskQueue: Set<Task>): Task | null {
  return null;

/** Performs Task steps */
function performTaskSteps(steps: Steps) {

 * Performs a microtask checkpoint
 * https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
function performMicrotaskCheckpoint(eventLoop: EventLoop) {
  // 1. If the event loop's performing a microtask checkpoint is true, then return.
  if (eventLoop.performingAMicrotaskCheckpoint) {
  // 2. Set the event loop's performing a microtask checkpoint to true.
  eventLoop.performingAMicrotaskCheckpoint = true;
  // 3. While the event loop's microtask queue is not empty:
  while (eventLoop.microtaskQueue.length > 0) {
    // 3.1. Let oldestMicrotask be the result of dequeuing from the event loop's microtask queue.
    const oldestMicrotask = eventLoop.microtaskQueue.shift();
    // 3.2. Set the event loop's currently running task to oldestMicrotask.
    eventLoop.currentRunningTask = oldestMicrotask;
    // 3.3. Run oldestMicrotask.
    // 3.4. Set the event loop's currently running task back to null.
    eventLoop.currentRunningTask = null;
  // 4. For each environment settings object whose responsible event loop is this event loop, notify about rejected
  //    promises on that environment settings object.
  // ...
  // 5. Cleanup Indexed Database transactions.
  // ...
  // 6. Perform ClearKeptObjects().
  // ...
  // 7. Set the event loop's performing a microtask checkpoint to false.
  eventLoop.performingAMicrotaskCheckpoint = false;

 * Runs `Update the rendering` steps for WindowEventLoop
 * https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering
function updateRendering(eventLoop: WindowEventLoop) {
  // ... reveal that Document
  // ... flush autofocus candidates for that Document
  // ... run the resize steps for that Document
  // ... run the scroll steps for that Document
  // ... evaluate media queries and report changes for that Document
  // ... update animations and send events for that Document
  // ... run the fullscreen steps for that Document
  // ... run the animation frame callbacks for that Document
  // ... if the focused area of that Document is not a focusable area, then run the focusing steps for that
  //     Document's viewport
  // ... perform pending transition operations for that Document
  // ... run the update intersection observations steps for that Document
  // ... invoke the mark paint timing algorithm
  // ... update the rendering or user interface of that Document and its node navigable
  // ... run process top layer removals given Document.

To summarize the above

The term Event Loop exists, but it is outside the area of responsibility of the ECMA-262 specification.

The official source of information for the Event Loop can be considered the HTML specification in the case of browser-based environments, or the official documentation of libraries or the HOST executors themselves in the case of non-browser-based environments.

The ECMA-262 specification contains indirect references to the processes associated with the Event Loop, leaving the implementation of these processes to the discretion of the HOST executor.

The Event Loop is not solely tied to the maintenance of JavaScript code. In fact, JavaScript is just one of the types of tasks that can enter the Event Loop. In addition to JavaScript, the browser can place other tasks here, such as tokenizing received HTML text, processing input/output operations, rendering elements on the screen, and much more.

According to the HTML specification, the Event Loop does not have to be thread-safe, but it can be. In the case of the execution of several agents with their Event Loop in the same thread, they must organize the algorithm of interaction with each other so that only one of them appears as an unblocked agent at one time, while the rest must be in a blocking state.

The Event Loop consists of a task queue and a microtask queue. Tasks assigned by the HOST executor are placed into the task queue. The microtask queue is an optional opportunity for a task from the task queue to use the Web API of the executor in order to perform any of its specific asynchronous subtasks.

My telegram channels:

EN - https://t.me/frontend_almanac
RU - https://t.me/frontend_almanac_ru

Русская версия: https://blog.frontend-almanac.ru/event-loop-myths-and-reality