The JavaScript Runtime, Event Loop, and Async Code

See how browser JavaScript runs, how queued work resumes, and why asynchronous results can arrive in the wrong order for a UI.

On this page

The short answer

JavaScript on a page usually runs one piece of main-thread code at a time. The surrounding browser can handle timers, network activity, and input while that code is not running. When an operation is ready to continue, the browser schedules more JavaScript. The event loop coordinates when tasks, microtasks, and rendering opportunities can happen.

This explains two common surprises: a zero-delay timer does not interrupt current code, and a fast network response can still leave the interface frozen if your processing code blocks the main thread.

Mental model

Ask what is running now, what is waiting elsewhere, and what is queued to run next. Synchronous work occupies the current call stack. Browser operations may progress outside that stack. Promise reactions and other callbacks resume only at the appropriate scheduling point. The browser gets a chance to update the screen when the current work allows it.

System traceOne simplified browser execution cycleCurrent synchronous code runs to completion; queued microtasks run at a checkpoint; another runnable task and a rendering opportunity may follow. Browsers can use multiple task sources and may skip a frame.
  1. Call stackRun the current JavaScript task
  2. Browser workTimers, fetches, and input can progress outside that stack
  3. Microtask checkpointRun queued Promise reactions and other microtasks
  4. Rendering opportunityUpdate a visible document when needed and possible
  5. Next taskChoose runnable work from an appropriate task source

JavaScript engine versus browser environment

The JavaScript engine evaluates the language: values, functions, expressions, and execution contexts. The browser supplies additional Web APIs such as the DOM, timers, and fetch. JavaScript itself does not contain a network stack or a document tree. Other environments, such as a server runtime, can run the language with a different set of APIs.

“JavaScript is single-threaded” is usually shorthand for one page's main JavaScript execution context. It does not mean the whole browser has one thread. Browsers perform work in other processes or threads and can also expose workers with separate execution contexts. The exact implementation is engine- and browser-dependent.

The call stack

When a function calls another function, the engine keeps track of the active execution contexts on a call stack. The called function runs and returns before the caller continues. Synchronous code in one task runs to completion before another task can take over that same execution context.MDN describes the stack and run-to-completion execution model.2

If a handler performs a large synchronous calculation, its stack remains busy. New clicks can be observed by the browser, but their main-thread handlers cannot run until that work yields or finishes.

Browser APIs

Calling setTimeout asks the browser to schedule a callback after a minimum delay. Calling fetch starts a request and returns a Promise. Registering an event listener tells the browser how to react to a later event. These calls do not mean the corresponding callback is running immediately.

Some work can proceed outside the current JavaScript stack, but asynchronous does not automatically mean your JavaScript calculations run in parallel. Once a callback starts on the main thread, it still uses that thread until it finishes or yields.

Tasks

A task is a unit of work selected by a browser event loop. Script execution, some event dispatches, and timer callbacks can arrive as tasks. The HTML Standard defines multiple task sources and selection rules; there is not one magical FIFO queue containing every possible event.The WHATWG HTML Standard defines event loops and task queues.1

For day-to-day reasoning, it is enough to know that the currently running task must finish before a timer callback can run in a later task on that same loop.

Microtasks

Microtasks include Promise reactions. They run at microtask checkpoints after the current JavaScript work unwinds, before the browser moves on to another ordinary task. A callback registered with .then() does not run at the moment it is registered, even if the Promise is already fulfilled.MDN explains Promise reactions and microtask checkpoints.3

The browser keeps processing microtasks until the relevant queue is empty. Code that endlessly schedules more microtasks can therefore delay input handling or rendering. Microtasks are not a free shortcut around main-thread work.

The event loop

The browser's event loop is the scheduling model that selects runnable work, performs microtask checkpoints, and allows rendering updates when appropriate. It is not a loop application developers call manually. The diagram above deliberately compresses a much more detailed specification.

One useful rule follows: the next callback cannot interrupt a long-running synchronous function on the same main thread. A timer can expire or a response can arrive, but their handlers still wait for a chance to run.

Timers

setTimeout(fn, 0) means “schedule fn after at least the applicable delay,” not “run fn now.” Current code and queued microtasks can run first. Browsers also apply timing rules and may delay timers in background tabs or when the main thread is busy.MDN documents why timer callbacks can run later than requested.5

Do not use a timer's nominal delay as a guarantee of exact execution time or as proof that work happened in parallel.

Promises

A Promise represents an eventual result or failure. A .then() reaction is scheduled when the Promise settles; it does not make an operation itself asynchronous. Wrapping CPU-heavy synchronous work in Promise.resolve().then(...) merely moves that work into a microtask. It can still block the main thread when it runs.

Predict this browser-console output before revealing it:

Predict the output order in a browserjs
console.log("first");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("last");
Reveal the output order and reason

first, last, promise, timer. Synchronous logging finishes in the current task. The resolved Promise's reaction runs as a microtask at the next checkpoint. The timer callback runs in a later task, not immediately at registration.

This is a controlled example without other scheduling work. Real applications have many task sources, but the stack/microtask/timer distinction remains useful.

async and await

An async function returns a Promise. At an await, that function's continuation pauses until the awaited value is ready, then resumes through Promise scheduling. The entire browser thread does not pause. Other eligible work can proceed while the function is suspended.MDN documents async function return values and await suspension.4

Code before the first await still runs synchronously when the function is called. Code after an await can also be expensive synchronous work once it resumes. await is a control-flow tool, not a performance guarantee.

Network requests

fetch() begins fetching and returns a Promise that resolves to a Response when response information is available. Reading and decoding the body may involve further asynchronous work. An HTTP error response such as 404 does not, by itself, reject the fetch Promise; code must inspect the response status.MDN documents Fetch's Promise and HTTP-error behavior.6

The APIs and JSON lesson explains request and response contracts. This lesson is about when a browser-side continuation can run after that exchange.

Rendering opportunities

After eligible work and a microtask checkpoint, a browser may update a visible document if a frame is needed and possible. It does not promise to paint between every two lines of code or after every task. The HTML Standard accounts for rendering opportunities and permits skipping unnecessary updates.The standard defines rendering updates within the event-loop model.1

This is why setting a loading indicator and immediately running a long synchronous loop may leave the indicator unseen until the loop ends: JavaScript changed state, but the browser did not yet get a useful opportunity to paint.

Long-running JavaScript

Large parsing, sorting, or transformation work on the main thread can delay clicks, keyboard responses, and visual updates. Network time and CPU time are different boundaries. Chrome DevTools' Performance panel can show main-thread tasks and help locate the expensive function; its particular UI is Chrome-specific, while the main-thread concern is broader.Chrome documents main-thread traces and long-task markers.7

Reducing work, splitting work into smaller units, or moving suitable computation to a worker can help, depending on the task. None is a universal replacement for measuring what actually blocks the page.

Completion order and race conditions

Suppose a search interface sends a request for ca, then a request for cat. The cat response arrives first and displays useful results. If the older ca response arrives later and blindly updates the same UI, it replaces newer results with stale ones. Request order did not determine response order.

Possible defenses include canceling an obsolete fetch, tagging each request with an identity and ignoring older results, or checking that a response still matches the current query. Cancellation is one option, not a guarantee that no already-started work can complete.AbortController can abort an in-flight Fetch operation.8 The later data-fetching lesson will cover loading, errors, and caching more fully.

Common misconceptions

Async code runs at the same time as all other JavaScript

Asynchronous operations can wait or progress outside the current stack. Their JavaScript continuations still need a scheduling opportunity, and CPU-heavy continuations can block the main thread.

Await freezes the page

await suspends one async function's continuation. The page can process other work while it waits, unless other main-thread work is blocking it.

Debugging scenario

Debugging scenario

A button starts a network request. While the request is pending, the interface responds normally. The response arrives, then the application performs a large synchronous transformation of its data before displaying results. The page stops responding to input during that transformation.

The request may have been quick enough; the freeze is in main-thread processing after the response. Inspect the Network timing and a performance trace separately. Find the transformation and measure its cost before changing API infrastructure. Moving the transformation behind an async keyword without changing the CPU work will not, by itself, free the main thread.

Why this matters when working with AI-generated code

Generated code can await fetch() correctly yet still overwrite newer search results with an older response. It can also put a large synchronous loop inside a Promise callback and call that “nonblocking.” Review the order in which requests start, finish, and update UI, and identify CPU work that occupies the main thread. Syntax that looks asynchronous is not proof of responsive behavior.

Knowledge check

Reflect, then reveal each answer.

  1. Why does setTimeout(fn, 0) not call fn immediately?

    It schedules a later task. The current synchronous work and applicable microtasks finish first, and browser timing rules can add delay.

  2. When does a .then() reaction on an already resolved Promise run?

    At a microtask checkpoint after the current JavaScript stack unwinds, not during registration.

  3. Does await block all browser interaction?

    No. It suspends that async function's continuation. Other work can run unless the main thread is occupied by separate synchronous work.

  4. Why can older search results replace newer ones?

    Requests can complete out of order. If every response updates the UI without checking whether it is still current, a later-arriving older result can overwrite a newer one.

  5. A page freezes only after a fast response arrives. What should you inspect?

    Inspect synchronous processing after the response, using a main-thread performance trace. The network may have succeeded while CPU work blocks input and rendering.

What to learn next

How this connects

  1. CSS Layout and Responsive Interfaces

    Next in the curriculum: see how style decisions create geometry across screen sizes. This lesson is planned, not yet published.

  2. Data Fetching, Loading, Errors, and Caching

    Later, handle requests as UI states and make stale-response behavior explicit. This lesson is planned.

  3. Frontend performance

    Later, connect main-thread work, bundles, and user-facing performance signals. This lesson is planned.

Key takeaway

When async UI behaves strangely, separate operation start, completion, scheduled continuation, and main-thread work. Their order—not just their syntax—determines what the user sees.

References & further reading

References & further reading8 sourcesPrimary standards and official documentation used for this lesson.
  1. HTML Standard — Web application APIs and event loops (opens in a new tab)

    WHATWG

    Browser event loops, task sources, microtask checkpoints, and rendering opportunities

  2. JavaScript execution model (opens in a new tab)

    MDN Web Docs

    Execution contexts, stack, jobs, and run-to-completion behavior

  3. Using microtasks in JavaScript with queueMicrotask() (opens in a new tab)

    MDN Web Docs

    Promise reactions, microtask order, and rendering starvation

  4. async function (opens in a new tab)

    MDN Web Docs

    Promise-returning async functions and await suspension

  5. Window: setTimeout() method (opens in a new tab)

    MDN Web Docs

    Timer scheduling and reasons callbacks run later than requested

  6. Window: fetch() method (opens in a new tab)

    MDN Web Docs

    Promise-based fetching and response availability

  7. Analyze runtime performance (opens in a new tab)

    Chrome for Developers

    Chrome DevTools main-thread traces and long-task diagnosis

  8. AbortController: abort() method (opens in a new tab)

    MDN Web Docs

    Canceling an in-flight fetch as one stale-result strategy

Return to the learning path