DEV Community

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

Posted on

JavaScript Event Loop: How Does JavaScript Handle Multiple Tasks?

JavaScript is often called single-threaded.

That means it has one main thread and can execute one piece of JavaScript at a time.

So here is the obvious question:

If JavaScript can do only one thing at a time, how can it handle all of this?

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

For example:

console.log("Start");

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

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

The output is:

Start
End
Timer finished
Enter fullscreen mode Exit fullscreen mode

But why?

If JavaScript waits for the timer to finish, shouldn't the output be:

Start
Timer finished
End
Enter fullscreen mode Exit fullscreen mode

The answer lies in one of the most important concepts in JavaScript:

The Event Loop

To understand it properly, we need to understand the complete system:

JavaScript Runtime
        │
        ├── Call Stack
        │
        ├── Web APIs
        │
        ├── Callback Queue
        │
        ├── Microtask Queue
        │
        └── Event Loop
Enter fullscreen mode Exit fullscreen mode

Let's break it down step by step.


1. The Call Stack — Where JavaScript Executes Code

The Call Stack is where JavaScript executes functions.

Think of it as a stack of tasks.

The last task added to the stack is the first one to be executed.

For example:

function first() {
  second();
}

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

first();
Enter fullscreen mode Exit fullscreen mode

The Call Stack roughly works like this:

Call Stack

first()
   ↓
second()
   ↓
console.log("Hello")
Enter fullscreen mode Exit fullscreen mode

As each function finishes, it is removed from the stack.

first() added
   ↓
second() added
   ↓
console.log() runs
   ↓
second() removed
   ↓
first() removed
Enter fullscreen mode Exit fullscreen mode

This is why JavaScript is synchronous by default.

It executes one operation at a time.

Task 1
  ↓
Task 2
  ↓
Task 3
Enter fullscreen mode Exit fullscreen mode

The next task doesn't start until the current task is complete.

But what happens when JavaScript encounters something that takes time?

For example:

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

JavaScript does not sit on the Call Stack for two seconds.

That would block everything else.

Instead, it hands the timer to the browser.


2. Web APIs — Where the Browser Handles Asynchronous Work

The browser provides powerful features called Web APIs.

These include:

  • setTimeout
  • fetch
  • DOM events
  • addEventListener
  • Geolocation
  • And many other browser capabilities

Consider this:

console.log("Start");

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

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

Here is what happens.

First:

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

is pushed onto the Call Stack and executed.

Output:

Start
Enter fullscreen mode Exit fullscreen mode

Then JavaScript reaches:

setTimeout(callback, 2000);
Enter fullscreen mode Exit fullscreen mode

The timer is registered with the browser's Web APIs.

Call Stack
     │
     │ setTimeout(...)
     ▼
   Web APIs
     │
     │ Wait 2 seconds
     ▼
 Callback is ready
Enter fullscreen mode Exit fullscreen mode

The Call Stack is now free to continue executing the rest of the code.

So:

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

runs immediately.

Output:

Start
End
Enter fullscreen mode Exit fullscreen mode

After two seconds, the timer's callback is ready.

But it still cannot directly jump back onto the Call Stack.

It first has to wait in a queue.


3. Callback Queue — Where Regular Async Callbacks Wait

Once an asynchronous operation is complete, its callback can be placed into the Callback Queue, also called the Task Queue.

Web APIs
   │
   │ Async operation completes
   ▼
Callback Queue
   │
   │ Waiting...
   ▼
Call Stack
Enter fullscreen mode Exit fullscreen mode

Let's look at the timer again:

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

After two seconds:

Callback Queue

[ console.log("Timer finished") ]
Enter fullscreen mode Exit fullscreen mode

But the callback cannot execute yet.

It has to wait until the Call Stack is empty.

This is where the Event Loop comes in.


4. The Event Loop — The Traffic Controller

The Event Loop constantly checks:

Is the Call Stack empty?
Enter fullscreen mode Exit fullscreen mode

If the answer is no, JavaScript keeps executing the current code.

If the answer is yes, the Event Loop can move waiting work into the Call Stack.

You can think of it like this:

          Is Call Stack Empty?
                 │
        ┌────────┴────────┐
        │                 │
       No                Yes
        │                 │
        ▼                 ▼
 Keep executing      Check queues
 current code             │
                          ▼
                   Move next task
                   to the Call Stack
Enter fullscreen mode Exit fullscreen mode

So the full timer flow becomes:

setTimeout(...)
      ↓
    Web APIs
      ↓
Wait 2 seconds
      ↓
Callback Queue
      ↓
Event Loop checks Call Stack
      ↓
Call Stack is empty
      ↓
Callback executes
Enter fullscreen mode Exit fullscreen mode

That is why the callback runs later.


5. But There Is Another Queue: Microtasks

This is where the Event Loop becomes more interesting.

JavaScript does not have only one queue.

There is also a Microtask Queue.

Microtasks usually include:

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

For example:

console.log("Start");

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

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

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

What do you think the output will be?

Start
End
Promise
Timeout
Enter fullscreen mode Exit fullscreen mode

Many developers expect setTimeout(..., 0) to run first.

But it doesn't.

Why?

Because Microtasks have higher priority than regular tasks.

The Event Loop checks the Microtask Queue before taking the next regular callback from the Callback Queue.


6. Microtasks Get Priority

Let's follow the previous example step by step.

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: console.log("Start")

The Call Stack executes it.

Output:
Start
Enter fullscreen mode Exit fullscreen mode

Step 2: Promise callback is registered

The Promise callback is placed in the Microtask Queue when ready.

Microtask Queue

[ console.log("Promise") ]
Enter fullscreen mode Exit fullscreen mode

Step 3: setTimeout is registered

The browser handles the timer.

Once ready, its callback goes to the regular queue.

Callback Queue

[ console.log("Timeout") ]
Enter fullscreen mode Exit fullscreen mode

Step 4: console.log("End")

The synchronous code continues.

Output:
Start
End
Enter fullscreen mode Exit fullscreen mode

Now the Call Stack becomes empty.

The Event Loop checks the queues.

But it does not take the regular callback first.

It checks:

1. Microtask Queue
2. Callback Queue
Enter fullscreen mode Exit fullscreen mode

So:

Promise
Enter fullscreen mode Exit fullscreen mode

runs first.

Then:

Timeout
Enter fullscreen mode Exit fullscreen mode

runs.

Final output:

Start
End
Promise
Timeout
Enter fullscreen mode Exit fullscreen mode

7. The Complete Event Loop Flow

Here is the simplified mental model:

                    JavaScript Code
                           │
                           ▼
                      Call Stack
                           │
              ┌────────────┼────────────┐
              │                         │
        Synchronous Code          Async Operation
              │                         │
              ▼                         ▼
         Execute Now                 Web APIs
                                        │
                         ┌──────────────┴──────────────┐
                         │                             │
                         ▼                             ▼
                  Microtask Queue                Callback Queue
                  Promise.then()                 setTimeout()
                  queueMicrotask()               Events
                                                Other tasks
                         │                             │
                         └──────────────┬──────────────┘
                                        ▼
                                   Event Loop
                                        │
                              Is Call Stack Empty?
                                        │
                                        ▼
                           Process Microtasks First
                                        │
                                        ▼
                           Then Process Next Task
Enter fullscreen mode Exit fullscreen mode

This is the key rule to remember:

Synchronous code → Microtasks → Regular tasks


8. A Real Example That Confuses Many Developers

Look at this:

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 the output be?

1
4
3
2
Enter fullscreen mode Exit fullscreen mode

Let's understand why.

First, synchronous code runs:

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

Output:

1
4
Enter fullscreen mode Exit fullscreen mode

Next, the Call Stack becomes empty

The Event Loop checks the Microtask Queue first.

So:

3
Enter fullscreen mode Exit fullscreen mode

runs.

Finally, the regular Callback Queue is processed.

So:

2
Enter fullscreen mode Exit fullscreen mode

runs.

The final result:

1
4
3
2
Enter fullscreen mode Exit fullscreen mode

This single example explains a huge part of how the Event Loop works.


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

No.

This is a common misunderstanding.

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

The 0 does not mean:

"Run this immediately."

It means:

"Run this callback after at least the specified delay, when the browser gets a chance to schedule it and the Call Stack is available."

So this:

console.log("Start");

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

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

still produces:

Start
End
Timeout
Enter fullscreen mode Exit fullscreen mode

Because the synchronous code must finish first.

The callback also has to wait for its turn in the Event Loop.


10. Why Can JavaScript Still Feel Like It Does Multiple Things at Once?

Because JavaScript doesn't necessarily perform every operation itself.

It can delegate work.

Think of JavaScript as a manager.

JavaScript
    │
    ├── "Browser, handle this timer."
    │
    ├── "Browser, wait for this click."
    │
    ├── "Network, fetch this data."
    │
    └── Continues executing other code
Enter fullscreen mode Exit fullscreen mode

While those operations happen outside the Call Stack, JavaScript can continue doing other work.

When the result is ready:

Async operation completes
        ↓
Callback enters a queue
        ↓
Event Loop waits for the right moment
        ↓
Callback enters the Call Stack
        ↓
JavaScript executes it
Enter fullscreen mode Exit fullscreen mode

So JavaScript is still executing one thing at a time on its main thread.

The surrounding environment makes asynchronous behavior possible.


11. Why Does the Event Loop Matter in Real Applications?

The Event Loop is not just an interview concept.

It directly affects how applications behave.

For example, if you run a heavy task:

while (true) {
  // Heavy work
}
Enter fullscreen mode Exit fullscreen mode

The Call Stack never becomes available.

That means:

  • Button clicks cannot be processed
  • Timers cannot run
  • Promise callbacks cannot execute
  • The page can become unresponsive

The Event Loop is effectively blocked because JavaScript is still busy.

This is why long-running JavaScript tasks can freeze a webpage.

Understanding the Event Loop helps you understand:

  • Why a UI becomes unresponsive
  • Why timers don't always run exactly on time
  • Why Promises often execute before setTimeout
  • How asynchronous JavaScript actually works
  • Why blocking the main thread is expensive

The Simplest Way to Remember Everything

Think of the system as a restaurant.

Call Stack
= The chef

Web APIs
= The kitchen equipment handling long tasks

Microtask Queue
= High-priority orders

Callback Queue
= Regular orders

Event Loop
= The manager checking what the chef should do next
Enter fullscreen mode Exit fullscreen mode

The chef can only prepare one thing at a time.

While the chef is working, other tasks can be handled elsewhere.

When the chef becomes free:

1. High-priority orders are checked first
2. Then regular orders are handled
Enter fullscreen mode Exit fullscreen mode

That is the Event Loop in a simple mental model.


Final Mental Model

Whenever you see asynchronous JavaScript, remember this:

JavaScript starts executing
        ↓
Synchronous code runs on the Call Stack
        ↓
Async work is delegated to Web APIs
        ↓
Completed callbacks wait in queues
        ↓
Call Stack becomes empty
        ↓
Event Loop checks Microtasks first
        ↓
Microtasks execute
        ↓
Then regular callbacks execute
Enter fullscreen mode Exit fullscreen mode

Or even more simply:

Call Stack
    ↓
Web APIs
    ↓
Microtasks / Callback Queue
    ↓
Event Loop
    ↓
Call Stack
Enter fullscreen mode Exit fullscreen mode

JavaScript may be single-threaded, but with the help of the browser, queues, and the Event Loop, it can handle a huge number of asynchronous operations without stopping your entire application.

That is what makes modern JavaScript applications feel fast and responsive.


Top comments (0)