DEV Community

Cover image for JavaScript Event Loop: How Does JavaScript Handle Multiple Tasks?
Tanu Priya
Tanu Priya

Posted on Edited on

JavaScript Event Loop: How Does JavaScript Handle Multiple Tasks?

JavaScript is often described as a single-threaded language.

At a high level, this means JavaScript executes JavaScript code on a single main thread, handling one piece of JavaScript execution at a time.

But modern applications constantly perform operations that take time:

  • API requests
  • Timers
  • Button clicks
  • User input
  • File operations
  • Animations

So an important question comes up:

If JavaScript executes one piece of code at a time, how can it handle asynchronous operations without freezing the application?

The answer is the Event Loop.

To understand it, we need to look at the different pieces involved:

             JavaScript Runtime

                  Call Stack
                      │
                      ▼
              JavaScript executes
                      │
          ┌───────────┴───────────┐
          │                       │
     Synchronous              Async work
       code                      │
                                  ▼
                              Host APIs
                                  │
                       ┌──────────┴──────────┐
                       ▼                     ▼
                Microtask Queue        Task Queue
                       │                     │
                       └──────────┬──────────┘
                                  ▼
                             Event Loop
                                  │
                                  ▼
                             Call Stack
Enter fullscreen mode Exit fullscreen mode

Let's understand how this works.


1. The Call Stack

The Call Stack is where JavaScript keeps track of the functions it is currently executing.

Consider:

function first() {
  second();
}

function second() {
  console.log("Hello");
}

first();
Enter fullscreen mode Exit fullscreen mode

When first() is called, it goes onto the Call Stack.

Then first() calls second(), so second() is added on top.

Conceptually:

Call Stack

┌─────────────────┐
│    second()     │
├─────────────────┤
│     first()     │
└─────────────────┘
Enter fullscreen mode Exit fullscreen mode

second() finishes first, so it is removed.

Then first() finishes.

This follows the LIFO principle:

Last In, First Out.

The important thing is that JavaScript cannot execute two pieces of JavaScript code simultaneously on the same main execution thread.

So if one task keeps the Call Stack busy for a long time, other JavaScript work has to wait.


2. What Happens With Asynchronous Work?

Now consider:

console.log("Start");

setTimeout(() => {
  console.log("Timer finished");
}, 2000);

console.log("End");
Enter fullscreen mode Exit fullscreen mode

You might initially expect:

Start
Timer finished
End
Enter fullscreen mode Exit fullscreen mode

But the actual output is:

Start
End
Timer finished
Enter fullscreen mode Exit fullscreen mode

Why doesn't JavaScript wait for two seconds?

Because the timer does not remain on the Call Stack.

The surrounding environment, such as the browser, provides APIs that can handle operations like timers and network requests.

A simplified flow looks like this:

Call Stack
    │
    │ setTimeout(...)
    ▼
Host / Browser APIs
    │
    │ wait for timer
    ▼
Timer becomes ready
    │
    ▼
Task Queue
Enter fullscreen mode Exit fullscreen mode

Meanwhile, JavaScript is free to continue executing:

console.log("End");
Enter fullscreen mode Exit fullscreen mode

That's why "End" appears before "Timer finished".


3. Host APIs Handle Asynchronous Operations

The browser provides many capabilities outside the JavaScript Call Stack.

Examples include:

  • Timers
  • DOM events
  • Network requests
  • Geolocation
  • Other browser functionality

For example:

setTimeout(() => {
  console.log("Done");
}, 2000);
Enter fullscreen mode Exit fullscreen mode

The timer is registered with the host environment.

JavaScript doesn't sit there doing nothing for two seconds.

Instead:

JavaScript
     │
     │ Register timer
     ▼
Browser / Host
     │
     │ Timer runs
     ▼
Callback becomes ready
     │
     ▼
Task Queue
Enter fullscreen mode Exit fullscreen mode

This is one of the key ideas behind asynchronous JavaScript:

JavaScript can delegate certain operations to its surrounding environment and continue executing other JavaScript code.


4. The Task Queue

When an asynchronous operation becomes ready, its callback may be scheduled as a task.

For example:

setTimeout(() => {
  console.log("Timeout");
}, 0);
Enter fullscreen mode Exit fullscreen mode

Once the timer is eligible to fire, its callback is placed into the appropriate task queue.

Conceptually:

Task Queue

┌──────────────────────────┐
│ console.log("Timeout")   │
└──────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

But being in the queue doesn't mean the callback executes immediately.

The Call Stack must be available, and the runtime's scheduling rules determine when the task gets a chance to run.

That's where the Event Loop comes in.


5. The Event Loop

The Event Loop coordinates when queued work gets an opportunity to execute.

A simplified mental model is:

        Call Stack
            │
            ▼
       Is it empty?
            │
       ┌────┴────┐
       │         │
      No        Yes
       │         │
       ▼         ▼
    Continue   Process
    execution  queued work
Enter fullscreen mode Exit fullscreen mode

When JavaScript is busy executing code, queued callbacks have to wait.

When the current execution finishes, the runtime can process pending asynchronous work according to its scheduling rules.

This continuous coordination is what we call the Event Loop.


6. Microtasks: The Important Second Queue

Things become more interesting when Promises are involved.

Consider:

console.log("Start");

Promise.resolve().then(() => {
  console.log("Promise");
});

setTimeout(() => {
  console.log("Timeout");
}, 0);

console.log("End");
Enter fullscreen mode Exit fullscreen mode

The output is:

Start
End
Promise
Timeout
Enter fullscreen mode Exit fullscreen mode

Why?

Because Promise reactions are scheduled as microtasks.

Examples of microtasks include:

  • Promise.then()
  • Promise.catch()
  • Promise.finally()
  • queueMicrotask()

So we can simplify the model into two categories:

Microtask Queue
    │
    ├── Promise callbacks
    └── queueMicrotask()

Task Queue
    │
    ├── Timers
    ├── User interaction tasks
    └── Other scheduled tasks
Enter fullscreen mode Exit fullscreen mode

7. Why Does the Promise Run Before setTimeout?

Let's break down the previous example.

console.log("Start");

Promise.resolve().then(() => {
  console.log("Promise");
});

setTimeout(() => {
  console.log("Timeout");
}, 0);

console.log("End");
Enter fullscreen mode Exit fullscreen mode

Step 1: Synchronous code executes

First:

console.log("Start");
Enter fullscreen mode Exit fullscreen mode

Output:

Start
Enter fullscreen mode Exit fullscreen mode

Then the Promise reaction is scheduled as a microtask.

The timer is also registered.

Finally:

console.log("End");
Enter fullscreen mode Exit fullscreen mode

runs.

Output so far:

Start
End
Enter fullscreen mode Exit fullscreen mode

Now the current JavaScript execution has finished.


Step 2: Microtasks are processed

The Promise callback is waiting in the Microtask Queue:

Microtask Queue

[ Promise callback ]
Enter fullscreen mode Exit fullscreen mode

The runtime processes pending microtasks before moving on to the next task.

So:

Promise
Enter fullscreen mode Exit fullscreen mode

is printed.


Step 3: The timer task gets its turn

The timer callback is waiting as a task.

So:

Timeout
Enter fullscreen mode Exit fullscreen mode

is printed.

Final output:

Start
End
Promise
Timeout
Enter fullscreen mode Exit fullscreen mode

The key idea is:

After the current JavaScript execution finishes, pending microtasks are processed before the next task gets a chance to run.


8. A Classic Event Loop Question

Consider:

console.log("1");

setTimeout(() => {
  console.log("2");
}, 0);

Promise.resolve().then(() => {
  console.log("3");
});

console.log("4");
Enter fullscreen mode Exit fullscreen mode

What will be printed?

1
4
3
2
Enter fullscreen mode Exit fullscreen mode

Let's see why.

Synchronous code

console.log("1");
console.log("4");
Enter fullscreen mode Exit fullscreen mode

produces:

1
4
Enter fullscreen mode Exit fullscreen mode

Then the current execution finishes.

The Promise callback is a microtask:

Microtask Queue
    ↓
    3
Enter fullscreen mode Exit fullscreen mode

The timer callback is a task:

Task Queue
    ↓
    2
Enter fullscreen mode Exit fullscreen mode

Microtasks are processed first:

3
Enter fullscreen mode Exit fullscreen mode

Then the timer task runs:

2
Enter fullscreen mode Exit fullscreen mode

Therefore:

1
4
3
2
Enter fullscreen mode Exit fullscreen mode

9. Does setTimeout(..., 0) Mean "Run Immediately"?

No.

This is one of the most common misconceptions about JavaScript timers.

Consider:

setTimeout(() => {
  console.log("Hello");
}, 0);
Enter fullscreen mode Exit fullscreen mode

The 0 does not mean:

Run this callback immediately.

It means the timer has a minimum delay of approximately zero milliseconds before the callback becomes eligible to be scheduled.

It still has to wait for:

Current JavaScript execution
        ↓
Microtasks
        ↓
Scheduling opportunity
        ↓
Timer callback
Enter fullscreen mode Exit fullscreen mode

For example:

console.log("Start");

setTimeout(() => {
  console.log("Timeout");
}, 0);

console.log("End");
Enter fullscreen mode Exit fullscreen mode

produces:

Start
End
Timeout
Enter fullscreen mode Exit fullscreen mode

Even with a zero-millisecond delay.


10. What If JavaScript Is Busy?

Here's where the Event Loop becomes especially important.

Consider:

console.log("Start");

setTimeout(() => {
  console.log("Timeout");
}, 0);

for (let i = 0; i < 10000000000; i++) {
  // Heavy computation
}

console.log("End");
Enter fullscreen mode Exit fullscreen mode

The timer may become ready while the loop is running.

But its callback cannot interrupt the currently executing JavaScript.

The Call Stack is still busy.

Conceptually:

Call Stack
    │
    ▼
Heavy computation
    │
    │
    │  Timer becomes ready
    │
    ▼
Task Queue
    │
    │ WAIT
    │
    ▼
Call Stack becomes available
    │
    ▼
Timer callback executes
Enter fullscreen mode Exit fullscreen mode

This is why a long-running JavaScript task can make a web page feel frozen.

Buttons may stop responding.

Animations can appear stuck.

Input can feel delayed.

Timers may execute later than expected.


11. JavaScript Isn't Doing Everything by Itself

A useful way to understand asynchronous JavaScript is to think of JavaScript as one worker coordinating with its environment.

For example:

             JavaScript
                  │
       ┌──────────┼──────────┐
       │          │          │
       ▼          ▼          ▼
     Timer      Network     Events
       │          │          │
       └──────────┼──────────┘
                  ▼
                Queues
                  │
                  ▼
              Event Loop
                  │
                  ▼
             Call Stack
Enter fullscreen mode Exit fullscreen mode

JavaScript doesn't need to sit on the Call Stack waiting for every external operation to finish.

The host environment can handle the operation and later schedule the appropriate work for JavaScript.

This is what allows applications to remain responsive while waiting for things such as network responses or user interactions.


12. Why the Event Loop Matters

The Event Loop isn't just an interview question.

It directly affects real applications.

UI responsiveness

If the main thread is blocked by expensive JavaScript, the browser has less opportunity to process user interactions and update the page.

API calls

Network operations can take unpredictable amounts of time. JavaScript can continue executing other work instead of blocking the entire application while waiting.

Timers

A timer specifies when its callback becomes eligible, not an exact guarantee that it will execute at that exact moment.

Promises

Understanding microtasks explains why Promise callbacks often run before timer callbacks.

Performance

Large synchronous tasks can delay everything waiting for the main thread.

Understanding the Event Loop helps developers reason about all of these behaviors.


13. The Simplified Mental Model

You don't need to memorize every implementation detail.

Start with this model:

             JavaScript
                  │
                  ▼
             Call Stack
                  │
        ┌─────────┴─────────┐
        │                   │
   Sync execution       Async operation
                            │
                            ▼
                       Host APIs
                            │
                   ┌────────┴────────┐
                   ▼                 ▼
             Microtask Queue     Task Queue
                   │                 │
                   └────────┬────────┘
                            ▼
                       Event Loop
                            │
                            ▼
                       Call Stack
Enter fullscreen mode Exit fullscreen mode

And remember the basic ordering:

Current JavaScript
        ↓
Microtasks
        ↓
Next task
Enter fullscreen mode Exit fullscreen mode

That's the core idea.


14. The Restaurant Analogy

Here's another simple way to remember it.

Imagine a restaurant.

Call Stack
= Chef

Host APIs
= Kitchen staff handling things in the background

Microtask Queue
= High-priority orders

Task Queue
= Regular orders

Event Loop
= Manager deciding what the chef should handle next
Enter fullscreen mode Exit fullscreen mode

The chef can prepare only one order at a time.

While something else is being prepared by the kitchen staff, the chef can work on another order.

When the chef becomes available, the manager coordinates which waiting work gets processed next.

The analogy isn't perfect, but it gives you the right intuition:

One execution thread doesn't mean the entire application has to stop whenever something takes time.


Final Mental Model

When you see asynchronous JavaScript, think:

1. JavaScript starts executing
             ↓
2. Synchronous code runs on the Call Stack
             ↓
3. Some operations are handled by the host environment
             ↓
4. Completed work is scheduled
             ↓
5. Current JavaScript execution finishes
             ↓
6. Microtasks are processed
             ↓
7. A task gets an opportunity to execute
             ↓
8. The cycle continues
Enter fullscreen mode Exit fullscreen mode

Or, in one line:

Call Stack
    ↓
Host APIs
    ↓
Microtasks / Tasks
    ↓
Event Loop
    ↓
Call Stack
Enter fullscreen mode Exit fullscreen mode

The most important thing to remember is that JavaScript's single-threaded execution model doesn't mean the application can perform only one kind of work at a time.

The JavaScript engine, host environment, queues, and Event Loop work together to coordinate asynchronous operations.

Once you understand that model, concepts like Promises, timers, API requests, event handlers, and UI responsiveness become much easier to reason about.

The Event Loop is essentially the mechanism that keeps asynchronous JavaScript moving without blocking every piece of work behind it.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.