The JavaScript event loop is a core concept that explains how JavaScript handles synchronous code, asynchronous operations, promises, and timers. Understanding the event loop helps you predict the order in which JavaScript statements execute.
JavaScript usually executes code on a single main thread. However, it can handle operations such as timers, network requests, and user interactions without blocking the main thread while those operations are waiting.
It achieves this through the call stack, Web APIs provided by the browser, task queues, and the event loop.
In this tutorial, you will learn how the JavaScript event loop works, how promises are scheduled, and why setTimeout() does not always execute at the exact time you specify.
What Is the Event Loop in JavaScript?
The event loop is a mechanism that coordinates the execution of JavaScript code and the processing of scheduled tasks.
JavaScript executes synchronous statements on the call stack. When asynchronous operations need to wait, the browser can handle them through Web APIs. Once their callbacks become eligible to run, they are scheduled for execution.
The event loop helps determine when queued callbacks can execute on the JavaScript thread.
For example:
console.log("First");
setTimeout(() => {
console.log("Second");
}, 1000);
console.log("Third");
Output:
First
Third
Second
The first and third messages appear before the second message because console.log() executes synchronously, while the timer callback runs later.
The event loop is essential for understanding why this happens.
Main Components of the JavaScript Event Loop
Several components work together to manage JavaScript execution.
| Component | Purpose |
|---|---|
| Call Stack | Executes synchronous JavaScript functions |
| Web APIs | Provide browser capabilities such as timers, networking, and DOM events |
| Callback Queue | Commonly refers to the task queue for callbacks such as timer and event callbacks |
| Microtask Queue | Holds microtasks, including promise reaction callbacks |
| Event Loop | Coordinates when queued work can execute |
| JavaScript Engine | Executes JavaScript code |
The browser’s event loop is more sophisticated than a single queue. For learning purposes, these components provide a useful model of how asynchronous JavaScript works.
Call Stack in JavaScript
The call stack tracks function calls that are currently executing. It follows the Last In, First Out (LIFO) principle, meaning the most recently added function is the first one to finish and leave the stack.
When JavaScript calls a function, it creates an execution context and places it on the stack. When that function returns, its execution context is removed.
Example of the Call Stack
function first() {
console.log("First function");
second();
}
function second() {
console.log("Second function");
}
first();
console.log("Program finished");
Output:
First function
Second function
Program finished
How the Call Stack Works
The execution proceeds in this order:
- JavaScript calls
first(), placing it on the call stack. first()prints a message and callssecond().second()is placed on top of the stack.second()prints its message and returns.first()finishes and returns.- JavaScript executes the final
console.log()statement.
The call stack handles the execution of functions, but it does not itself wait for timers or network requests to finish.
Learn more about the JavaScript Call Stack.
Web APIs in JavaScript
Web APIs are browser-provided features that allow JavaScript to interact with the browser and its environment.
Examples include:
setTimeout()andsetInterval()for timers.fetch()for network requests.- DOM event handling for clicks and keyboard input.
- Browser interfaces for managing other asynchronous operations.
These APIs are not all part of the JavaScript language itself. They are provided by the browser environment.
Consider this example:
console.log("Start");
setTimeout(() => {
console.log("Timer finished");
}, 1000);
console.log("End");
Output:
Start
End
Timer finished
What Happens Behind the Scenes?
- JavaScript executes
console.log("Start"). - It encounters
setTimeout()and registers a timer with the browser. - JavaScript continues executing without waiting for the timer.
- It executes
console.log("End"). - After the timer becomes eligible and its callback can be scheduled, the callback waits for the JavaScript thread to become available.
- The callback executes and prints
Timer finished.
A timer callback cannot interrupt JavaScript code that is already running on the main thread.
Callback Queue in JavaScript
The callback queue is a common beginner-friendly name for the task queue, also known as the macrotask queue in many explanations.
It contains tasks that are ready to run when the JavaScript thread can process them. Examples include timer callbacks and many user-interaction event callbacks.
Consider this example:
console.log("Start");
setTimeout(() => {
console.log("Timer callback");
}, 0);
console.log("End");
Output:
Start
End
Timer callback
Even though the timer delay is 0, the callback does not execute immediately. The synchronous statements must finish first.
Why Does the Callback Wait?
The callback cannot run while the current synchronous JavaScript code is still executing.
After the current task finishes, the event loop can process eligible queued tasks. Before moving to another task, the browser normally performs a microtask checkpoint, processing pending microtasks until the queue is empty, subject to the browser’s event-loop rules.
Microtask Queue in JavaScript
The microtask queue holds callbacks that should run at a microtask checkpoint. Promise reaction callbacks created by .then(), .catch(), and .finally() are common examples.
Other microtasks can be scheduled using queueMicrotask().
Microtasks are processed after the current JavaScript task finishes and before the event loop proceeds to another ordinary task.
Example of the Microtask Queue
console.log("Start");
Promise.resolve().then(() => {
console.log("Promise callback");
});
console.log("End");
Output:
Start
End
Promise callback
How It Works
console.log("Start")executes immediately.- The promise is already fulfilled, so its
.then()reaction is scheduled as a microtask. - JavaScript continues to
console.log("End"). - The current synchronous execution finishes.
- The event loop processes the queued microtask.
Promise callbackappears in the console.
The .then() callback does not execute immediately just because the promise is already fulfilled.
Common Sources of Microtasks
| Source | Example |
|---|---|
| Promise fulfillment | promise.then(callback) |
| Promise rejection | promise.catch(callback) |
| Promise completion handler | promise.finally(callback) |
| Explicit microtask scheduling | queueMicrotask(callback) |
The await keyword also uses promise-based scheduling for the continuation of an asynchronous function.
What Are Macrotasks in JavaScript?
Macrotask is a commonly used teaching term for ordinary tasks processed by the event loop. In browser terminology, these are generally called tasks.
Examples include:
- Timer callbacks registered through
setTimeout(). - Timer callbacks registered through
setInterval(). - Many DOM event callbacks.
- Certain message events and other browser-scheduled tasks.
Microtasks and ordinary tasks follow different scheduling rules.
A useful simplified model is:
- Execute the current task.
- Process pending microtasks until the microtask queue is empty.
- The browser may perform rendering work when appropriate.
- Select another eligible task and execute it.
- Repeat.
The browser’s exact scheduling and rendering behavior can vary, so this model is a guide rather than a guarantee that every cycle includes a rendering step.
Difference Between Microtasks and Macrotasks
The most important difference is when the callbacks become eligible to run relative to the current task and other queued work.
| Feature | Microtasks | Macrotasks (tasks) |
|---|---|---|
| Common examples | Promise callbacks, queueMicrotask() | Timer and many event callbacks |
| When processed | At microtask checkpoints | When selected by the event loop |
| Typical ordering | Before the next ordinary task | After the current task and its microtask checkpoint |
| Can be delayed by running code? | Yes | Yes |
| Can create more queued work? | Yes | Yes |
Example Comparing Both Queues
console.log("One");
setTimeout(() => {
console.log("Two");
}, 0);
Promise.resolve().then(() => {
console.log("Three");
});
console.log("Four");
Output:
One
Four
Three
Two
Why Is This the Output?
First, JavaScript executes the synchronous statements, printing One and Four.
The timer callback is scheduled as a task, while the promise callback is scheduled as a microtask.
After the current task finishes, JavaScript processes the promise microtask. The timer callback can then execute as a later task.
Therefore, the output order is:
OneFourThreeTwo
How Promises Run in the Event Loop
Promises are important because their reaction callbacks are scheduled as microtasks.
Creating a promise and running its executor are different from running its .then() callback. The promise executor passed to new Promise() executes synchronously, while .then(), .catch(), and .finally() callbacks run asynchronously as microtasks when scheduled.
Example Using a Promise
console.log("Start");
const promise = new Promise((resolve) => {
console.log("Promise executor");
resolve();
});
promise.then(() => {
console.log("Promise then");
});
console.log("End");
Output:
Start
Promise executor
End
Promise then
Step-by-Step Execution
Step 1: Execute synchronous code
JavaScript prints Start.
Step 2: Create the promise
The executor function runs immediately and prints Promise executor. It then calls resolve().
Step 3: Register the reaction
The .then() callback is scheduled as a microtask because the promise has fulfilled.
Step 4: Continue synchronous execution
JavaScript prints End.
Step 5: Process microtasks
After the current task finishes, the promise reaction runs and prints Promise then.
Promise Callbacks and setTimeout()
Consider this example:
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
Output:
Start
End
Promise
Timeout
The promise callback runs before the timer callback because the promise reaction is a microtask, while the timer callback is a task.
This is one of the most useful rules to remember when debugging asynchronous JavaScript.
How setTimeout() Runs in the Event Loop
The setTimeout() function schedules a callback to become eligible after a specified delay. It does not guarantee that the callback will execute at the exact requested time.
The callback must wait until the delay has passed and the event loop can select its task.
Example of setTimeout()
console.log("First");
setTimeout(() => {
console.log("Second");
}, 1000);
console.log("Third");
Output:
First
Third
Second
The timer is registered, and JavaScript continues to execute the synchronous statements. After the delay, the callback becomes eligible to run.
Why Does setTimeout(…, 0) Not Run Immediately?
A zero-millisecond delay does not mean that the callback runs before the next statement.
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
console.log("C");
Output:
A
C
B
The synchronous code finishes before the timer callback runs.
setTimeout() with a Promise
console.log("Start");
setTimeout(() => {
console.log("Timer");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("Finish");
Output:
Start
Finish
Promise
Timer
The event loop first allows the current synchronous task to finish. The promise reaction then runs during the microtask checkpoint, before the timer task is processed.
A Complete Event Loop Example
The following example combines synchronous code, a promise, setTimeout(), and queueMicrotask().
console.log("A");
setTimeout(() => {
console.log("B");
Promise.resolve().then(() => {
console.log("C");
});
}, 0);
Promise.resolve().then(() => {
console.log("D");
});
queueMicrotask(() => {
console.log("E");
});
console.log("F");
Output:
A
F
D
E
B
C
Explanation of the Output
A and F execute first.
Both statements are synchronous, so they execute during the current task.
D and E execute next.
The promise reaction and queueMicrotask() callback are queued as microtasks. They run in the order they were scheduled in this example.
B executes after the microtask checkpoint.
The timer callback runs as a later task.
C executes after B.
Inside the timer callback, the fulfilled promise schedules another microtask. Once the timer callback finishes, that microtask is processed before the event loop selects another ordinary task.
This example shows that microtasks can be created by callbacks that are themselves running as tasks.
Can Microtasks Block the Event Loop?
Yes. If microtasks continually schedule more microtasks, the microtask queue may never become empty.
For example:
function repeat() {
queueMicrotask(repeat);
}
repeat();
setTimeout(() => {
console.log("Timer executed");
}, 0);
This code continuously schedules additional microtasks. The microtask checkpoint may never finish, preventing the timer callback from getting a chance to run.
Do not run this example in your normal browser console unless you are prepared to close the page or interrupt execution.
For responsive applications, avoid long-running synchronous operations and unbounded chains of microtasks.
Common Mistakes About the JavaScript Event Loop
Assuming JavaScript Executes All Asynchronous Code Immediately
Asynchronous callbacks run later according to their scheduling rules. Registering a callback does not mean it executes at the registration point.
Assuming setTimeout() Guarantees an Exact Delay
The delay specifies when a timer can become eligible, not an exact execution time. Busy JavaScript threads, browser scheduling, and timer restrictions can delay execution.
Assuming Promise Executors Are Asynchronous
The executor passed to new Promise() runs synchronously. Promise reaction callbacks such as .then() are scheduled as microtasks.
Confusing the Callback Queue with the Microtask Queue
Promise reactions normally run as microtasks. Timer callbacks are scheduled as tasks. These queues follow different scheduling rules.
Assuming the Event Loop Runs Multiple JavaScript Statements at Once
In the usual browser main-thread model, one JavaScript task runs at a time. The event loop coordinates when additional work can execute; it does not make synchronous code execute simultaneously.
FAQs About the JavaScript Event Loop
What is the event loop in JavaScript?
The event loop coordinates JavaScript execution with queued tasks and microtasks. It allows the runtime to process asynchronous callbacks after the current synchronous execution has completed.
Why do promises run before setTimeout()?
Promise reaction callbacks are microtasks. After the current task finishes, pending microtasks are processed before the event loop selects another ordinary task, such as a timer callback.
Does setTimeout() with 0 milliseconds run immediately?
No. It schedules a timer callback that can run after the current task finishes and the callback becomes eligible. Other queued work may delay it further.
What is the difference between the call stack and the callback queue?
The call stack tracks functions that are currently executing. The callback or task queue holds tasks waiting for an opportunity to execute.
What is the difference between the microtask queue and the task queue?
The microtask queue contains work such as promise reactions. The task queue contains work such as timer callbacks and many event callbacks. Pending microtasks are normally processed before the event loop selects another ordinary task.
Are Web APIs part of JavaScript?
No. Web APIs are browser-provided capabilities, such as timers, DOM interfaces, and networking APIs. JavaScript interacts with these APIs through the browser environment.
Does the event loop make JavaScript multithreaded?
No. The event loop does not make main-thread JavaScript execute multiple tasks simultaneously. Browsers can perform certain operations outside the main thread and support Web Workers, but that is separate from the event loop’s task scheduling.
Conclusion
The JavaScript event loop explains how synchronous code and asynchronous callbacks work together. The call stack executes functions, Web APIs provide browser capabilities, and queues hold callbacks until they can run.
Remember these essential rules:
- Synchronous code executes first on the current call stack.
- Promise reactions and other microtasks run at microtask checkpoints.
- Timer callbacks run as tasks when they become eligible and the event loop selects them.
setTimeout()specifies a delay, not an exact execution time.- Microtasks scheduled during a task are processed after that task finishes, before the next ordinary task is selected.
Once you understand these rules, you can predict the execution order of many JavaScript programs and build more reliable asynchronous applications.