What Is An Event Loop In Javascript: A Beginner's Guide

- JavaScript normally executes one piece of code at a time on its current thread.
- Synchronous code runs immediately and remains on the call stack until it finishes.
- Browser or Node.js APIs manage operations such as timers, network activity, and file I/O.
- When an asynchronous operation is ready, its JavaScript callback is scheduled in a queue.
- Promise handlers and code after
await are usually scheduled as microtasks.- Microtasks run after the current task finishes and before the event loop begins the next task.
- A
setTimeout(..., 0) callback still waits until the current code and pending microtasks finish.- Long-running JavaScript blocks the event loop and can make an application appear frozen.
JavaScript code can start a timer, request data from a server, respond to a click, and update a web page without waiting for each operation to finish before moving to the next one. The event loop is the mechanism that coordinates when the JavaScript associated with these operations is allowed to run.
The concept often feels more complicated than it is because terms such as call stack, Web APIs, task queue, and microtask queue are introduced together. It becomes much easier to understand when we follow one piece of code from execution to completion.
When debugging JavaScript applications, we have found that understanding this sequence is especially helpful for explaining unexpected console output, delayed timers, frozen interfaces, and promises that execute before other callbacks.
This guide explains the JavaScript event loop in beginner-friendly terms, shows how tasks and microtasks are processed, and covers the performance problems developers should watch for.
What Is the JavaScript Event Loop?
The event loop is the scheduling process used by a JavaScript runtime to coordinate tasks, microtasks, asynchronous operations, and, in a browser, rendering.
JavaScript code does not normally execute several functions simultaneously on the same thread. One function begins, performs its work, and returns before another queued piece of JavaScript can run.
However, the runtime surrounding JavaScript can manage other operations separately. A browser provides timers, network requests, user events, and rendering. Node.js provides file-system operations, network I/O, timers, and other native capabilities.
When one of these operations is ready for JavaScript to respond, its callback is scheduled. The event loop decides when the JavaScript thread is free to execute that callback.
The event loop does not make synchronous JavaScript run in parallel. It allows the runtime to avoid waiting idly for external operations such as a network response before continuing with other JavaScript.
Is JavaScript Synchronous or Asynchronous?
JavaScript execution is synchronous by default. Statements in the current script normally run in order, and one function must finish before the next queued function can execute.
console.log("First");
console.log("Second");
console.log("Third");The output is predictable:
First
Second
ThirdJavaScript also provides access to asynchronous operations through its host environment. A timer can be started, for example, without stopping the rest of the script:
console.log("Start");
setTimeout(() => {
console.log("Timer finished");
}, 1000);
console.log("End");The output is:
Start
End
Timer finishedsetTimeout() registers a timer with the runtime and returns. JavaScript then continues to console.log("End"). Once the timer has elapsed, its callback becomes eligible to run, but it must still wait for the current JavaScript work to finish.
This distinction is important: the JavaScript language itself is not delegating every asynchronous operation to “the browser.” The surrounding runtime provides the relevant APIs. In a browser, these are commonly called Web APIs. In Node.js, different runtime components handle timers, networking, and I/O.
Key Components of the Event Loop
1. The Call Stack
The call stack records which JavaScript functions are currently executing. When a function is called, a frame representing that function is placed on the stack. When the function returns, its frame is removed.
Consider this example:
function greetUser() {
createMessage();
}
function createMessage() {
console.log("Hello");
}
greetUser();The global script begins first. greetUser() is then added to the stack, followed by createMessage(). After the message is logged, createMessage() returns, then greetUser() returns.
The call stack must finish its current work before another queued callback can execute. If a function contains an expensive loop or calculation, it remains on the stack and delays everything waiting behind it.
2. Runtime APIs
Operations such as timers, HTTP requests, button clicks, and file reads are not managed by the call stack alone. The browser or Node.js runtime provides APIs that can monitor these operations outside the current JavaScript execution.
For example:
setTimeout(handleTimeout, 1000);JavaScript calls setTimeout(), and the runtime begins tracking the timer. The call to setTimeout() itself finishes quickly. After at least one second, the runtime schedules handleTimeout to run.
The callback does not execute the instant the timer expires. It becomes eligible to execute once the event loop can select it and the JavaScript thread is available.
3. Task Queues
Callbacks from timers, user events, and several other browser operations are scheduled as tasks. These are sometimes called macrotasks, although the HTML standard primarily uses the term task.
Common sources of tasks include:
setTimeout()andsetInterval()callbacks- Click and keyboard event handlers
- Message events
- Initial script execution
For beginner explanations, these tasks are often shown inside one callback queue. Browser specifications actually allow multiple task queues and task sources, so the real scheduling model is more detailed than a single queue diagram suggests.
During an event-loop iteration, the browser selects a runnable task and executes it. After that task finishes, the runtime performs a microtask checkpoint before moving towards rendering or another task.
4. The Microtask Queue
Microtasks are short pieces of JavaScript scheduled to run after the current task completes but before the event loop proceeds to the next task.
Let’s Develop Your JavaScript Project Together!
We build fast, reliable, and scalable JavaScript applications that power modern businesses across the web.
Common microtask sources include:
- Promise handlers added with
.then(),.catch(), or.finally() - Code that resumes after
await queueMicrotask()MutationObservercallbacks in browsers
When the current task finishes, the runtime drains the microtask queue. If one microtask adds another, the newly added microtask also runs before the event loop moves to the next task.
This behaviour is why promise callbacks usually run before timer callbacks that are already eligible.
A fetch() request itself is not a microtask. The browser manages the network operation. When its promise settles, the corresponding promise handler is queued as a microtask.
5. Browser Rendering
In a browser, rendering also needs time on the main thread. After a task and its microtasks have completed, the browser may update layout, paint the page, and display a new frame.
If JavaScript keeps the main thread occupied for too long, the browser cannot respond promptly to input or render frames smoothly. The application may continue processing code correctly while appearing frozen to the user.
This is why event-loop performance is directly connected to interface responsiveness.
How Does the Event Loop Work?
A simplified browser event-loop cycle works like this:
- The runtime selects a task and executes its JavaScript.
- Functions called by that task are added to and removed from the call stack.
- Asynchronous browser operations continue outside the current JavaScript stack.
- When the current task finishes, the runtime drains the microtask queue.
- The browser may update rendering.
- The event loop selects another runnable task and repeats the process.
This is a simplified model, but it accurately explains the ordering developers encounter in everyday browser code.
JavaScript Event Loop Example
Consider the following code:
console.log("Start");
setTimeout(() => {
console.log("Timeout callback");
}, 0);
Promise.resolve().then(() => {
console.log("Promise resolved");
});
console.log("End");The output is:
Start
End
Promise resolved
Timeout callbackHere is what happens.
First, the initial script begins as a task. console.log("Start") executes immediately.
Next, setTimeout() registers a timer. A delay of zero does not mean that the callback executes immediately. It means the callback can be scheduled after the minimum timer delay, but it must still wait for the current task and earlier scheduling work to finish.
Promise.resolve().then() Registers a promise handler as a microtask. The script then executes console.log("End").
At this point, the initial script has finished. Before starting the timer task, the runtime drains the microtask queue. Therefore, Promise resolved appears before Timeout callback.
This example demonstrates the essential ordering:
Current synchronous code → Microtasks → Next taskHow async and await Use the Event Loop
async and await provide a cleaner way to work with promises, but they do not remove the event loop from the process.
async function loadData() {
console.log("Loading");
await Promise.resolve();
console.log("Loaded");
}
console.log("Before");
loadData();
console.log("After");The output is:
Before
Loading
After
LoadedCalling loadData() begins executing the function immediately. It logs Loading and reaches await.
The function pauses, allowing the surrounding synchronous code to continue. After is logged next. Once the awaited promise is settled, the remaining part of loadData() is scheduled as a microtask, producing Loaded.
await pauses the async function containing it; it does not block the entire JavaScript thread.
Why Does setTimeout(..., 0) Not Run Immediately?
The delay passed to setTimeout() specifies the minimum waiting period before its callback may be scheduled. It does not guarantee the exact execution time.
setTimeout(() => {
console.log("Timer");
}, 0);
for (let i = 0; i < 1_000_000_000; i++) {
// Expensive synchronous work
}The timer callback cannot run while the loop is occupying the JavaScript thread. It must wait for the current script to finish and for the event loop to reach the appropriate task.
Timers may also be delayed by other queued work, browser throttling, minimum-delay rules, or an inactive browser tab. They should not be treated as precise real-time schedulers.
Event Loop in Browsers vs Node.js
Browser and Node.js environments both use event loops, but their implementations and available APIs are not identical.
Browsers coordinate JavaScript tasks with user events, timers, networking, DOM updates, and rendering. Node.js structures its event loop around phases used for timers, I/O callbacks, polling, setImmediate(), and close callbacks.
Node.js also provides process.nextTick(). Although it is sometimes grouped with microtasks in simplified explanations, Node treats the next-tick queue separately and processes it before continuing through the event-loop phases. Excessive use can delay I/O and other queued work.
Similarly, setImmediate() is a Node.js API and is not a standard browser replacement for setTimeout().
When learning the event loop, it is helpful to understand the browser model first and then study the Node.js phases separately rather than mixing their queues into one diagram.
How Blocking Code Affects the Event Loop
The event loop cannot move to the next callback while synchronous JavaScript is still running. A long task therefore delays timers, promise continuations, input handlers, and browser rendering.
function blockThread() {
const end = Date.now() + 3000;
while (Date.now() < end) {
// Blocks for approximately three seconds
}
}
blockThread();During those three seconds, the page may not respond to clicks or update animations. The browser is not necessarily broken; its main thread is occupied.
This commonly happens because of large loops, expensive data transformations, synchronous storage or I/O APIs, complex DOM work, or third-party scripts executing for too long.
On the server, blocking the Node.js event loop can delay unrelated requests handled by the same process and reduce overall throughput.
Microtask Starvation
Microtasks run before the next task, and the runtime continues processing them until the microtask queue is empty. This creates a potential performance problem if each microtask schedules another one.
function repeat() {
queueMicrotask(repeat);
}
repeat();This code continuously adds another microtask. The runtime may have difficulty reaching timers, events, or rendering because the queue never becomes empty.
For this reason, microtasks should not be used simply because they receive earlier execution. They are suitable for short follow-up work that must run after the current task. Larger or non-urgent work should be scheduled in a way that allows the runtime to process other tasks and render the page.
Event Loop Performance Best Practices
Break Up Long-Running Work
If a calculation takes long enough to block user interactions, divide it into smaller pieces and allow the event loop to regain control between them.
Let’s Develop Your JavaScript Project Together!
We build fast, reliable, and scalable JavaScript applications that power modern businesses across the web.
For CPU-intensive browser work that should run separately from the UI thread, use a Web Worker. In Node.js, worker threads may be appropriate for expensive CPU-bound operations.
Asynchronous syntax alone does not make a calculation non-blocking. An async function containing a large synchronous loop will still occupy the thread until that loop finishes.
Use requestAnimationFrame() for Visual Updates
Visual changes that should align with browser painting are usually better scheduled with requestAnimationFrame() than with repeated timers.
function updateAnimation() {
// Update the next visual frame
requestAnimationFrame(updateAnimation);
}
requestAnimationFrame(updateAnimation);The browser can coordinate these callbacks with its rendering cycle, producing smoother animations and avoiding unnecessary work when a page is not visible.
Avoid Excessive Microtasks
Promise chains and queueMicrotask() are useful, but large self-extending microtask sequences can delay rendering and user events.
Keep microtask callbacks short. If work does not need to finish before the next event-loop task, avoid giving it microtask priority unnecessarily.
Batch DOM Changes
Repeatedly reading layout information and changing styles can force the browser to recalculate layout several times.
Group related DOM reads together, and then group writes where possible. This reduces layout thrashing and gives the browser a better opportunity to perform rendering efficiently.
Clean Up Long-Lived Resources
Remove event listeners, clear timers, abort unused network requests, and disconnect observers when their associated components are destroyed.
These resources do not always block the event loop directly, but leaving them active can create unnecessary callbacks, retain memory, and make application behaviour harder to predict.
Measure Before Optimising
Browser developer tools can show long tasks, frame delays, scripting time, rendering activity, and the call stacks responsible for blocking the main thread.
In production work, an interface that “feels slow” should be converted into a measurable problem. Identifying whether the delay comes from JavaScript execution, network activity, rendering, or memory pressure prevents time being spent optimising the wrong part of the application.
Common Event Loop Misconceptions
JavaScript Executes All Asynchronous Code in Parallel
The runtime may handle several external operations concurrently, but their JavaScript callbacks generally return to a thread where they execute one at a time.
A Zero-Millisecond Timer Executes Immediately
A zero delay makes the callback eligible after the timer requirement is satisfied. It still waits for the current task, pending microtasks, and relevant runtime scheduling.
Promises Run in the Background
The operation associated with a promise may involve background or asynchronous work, but the JavaScript inside .then() executes as a microtask on the JavaScript thread.
async Makes Every Function Non-Blocking
An async function can still block the thread if it performs expensive synchronous work. await yields only when execution reaches it and waits for the associated promise.
The Event Loop Is Part of the JavaScript Language Alone
The ECMAScript language defines jobs and promise behaviour, while browsers and Node.js provide their own event-loop implementations and host APIs. This is why available operations and some scheduling details differ between environments.
Why Understanding the Event Loop Matters
The event loop helps explain behaviour that might otherwise appear inconsistent. It shows why a promise handler runs before a timer, why an expired timer can still be delayed, and why a page stops responding during a large calculation.
This knowledge also improves architectural decisions. Developers can distinguish I/O-bound work from CPU-bound work, decide when a Web Worker is appropriate, and avoid filling high-priority queues with work that can wait.
Most importantly, understanding the event loop changes how performance problems are diagnosed. Instead of assuming that “JavaScript is slow,” developers can identify which task occupied the thread, what was delayed behind it, and how the work should be rescheduled or reduced.
Conclusion
The JavaScript event loop coordinates when queued JavaScript work is allowed to execute. Synchronous code runs on the call stack, host APIs manage operations such as timers and network requests, and completed work schedules tasks or microtasks for later execution.
After the current task finishes, the runtime processes pending microtasks before moving to another task. In browsers, rendering may take place between these stages when the main thread is available.
Understanding this order makes asynchronous JavaScript easier to predict and debug. It also explains why long-running code blocks interactions, why promises usually run before timer callbacks, and why asynchronous syntax does not automatically make CPU-heavy work run in the background.
Once this model is clear, callbacks, promises, async/awaittimers, and browser responsiveness stop feeling like separate concepts. They become different parts of the same scheduling system.



