Skip to main content

Command Palette

Search for a command to run...

Promise and timeout types interview questions and some theory

Updated
27 min read
D

I write here to learn, feel free to correct.

(on this page i have [3) Harder: await inside nested async flow] - do this always, it has tricky aspect

ok there are a lot of promise and timout output based question give 1

Here are 3, from easy to harder, with line-by-line traces.

1) Easy

console.log("A");

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

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

console.log("D");

What prints?

A
D
C
B

Trace

Line 1

console.log("A");

Runs immediately.

Output:

A

Line 2

setTimeout(() => {
  console.log("B");
}, 0);
  • timer is registered with the host environment

  • callback goes to the task/macrotask queue later

  • nothing prints yet

Line 3

Promise.resolve().then(() => {
  console.log("C");
});
  • resolved promise

  • .then(...) callback goes to the microtask queue

  • nothing prints yet

Line 4

console.log("D");

Runs immediately.

Output:

A
D

After sync code ends

Now stack is empty.

Event loop does:

  1. drain microtasks

  2. then run next macrotask

So:

  • print C

  • then print B

Final:

A
D
C
B

2) Medium

console.log(1);

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

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

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

console.log(6);

What prints?

1
6
3
5
4
2

Trace

Line 1

console.log(1);

Prints:

1

Line 2

setTimeout(() => console.log(2), 0);
  • timeout callback scheduled as macrotask

  • no output yet

Line 3

Promise.resolve().then(() => {
  console.log(3);
  Promise.resolve().then(() => {
    console.log(4);
  });
});
  • first promise callback goes into microtask queue

Microtask queue:

[ print 3 and schedule print 4 ]

Line 4

Promise.resolve().then(() => {
  console.log(5);
});
  • another microtask queued

Microtask queue:

[
  print 3 and schedule print 4,
  print 5
]

Line 5

console.log(6);

Prints:

1
6

Now sync code is done

Drain microtasks fully.

First microtask runs

console.log(3);

Prints:

3

Then inside it:

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

This queues another microtask.

Microtask queue is now:

[
  print 5,
  print 4
]

Next microtask runs

Prints:

5

Next microtask runs

Prints:

4

Only after all microtasks are done does macrotask run.

Timeout callback runs

Prints:

2

Final:

1
6
3
5
4
2

Key lesson

A microtask can queue another microtask, and the engine keeps draining microtasks before touching the timeout.


3) Harder

console.log("start");

setTimeout(() => {
  console.log("timeout-1");

  Promise.resolve().then(() => {
    console.log("promise-in-timeout");
  });
}, 0);

Promise.resolve().then(() => {
  console.log("promise-1");

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

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

console.log("end");

What prints?

start
end
promise-1
promise-2
timeout-1
promise-in-timeout
timeout-2

Trace

Line 1

console.log("start");

Prints:

start

Line 2

setTimeout(() => {
  console.log("timeout-1");

  Promise.resolve().then(() => {
    console.log("promise-in-timeout");
  });
}, 0);
  • schedules timeout-1 as a macrotask

Macrotask queue:

[ timeout-1 ]

Line 3

Promise.resolve().then(() => {
  console.log("promise-1");

  setTimeout(() => {
    console.log("timeout-2");
  }, 0);
});
  • queues microtask promise-1

Microtask queue:

[ promise-1 ]

Line 4

Promise.resolve().then(() => {
  console.log("promise-2");
});
  • queues microtask promise-2

Microtask queue:

[ promise-1, promise-2 ]

Line 5

console.log("end");

Prints:

start
end

Sync phase done

Now drain microtasks.

Microtask 1: promise-1

Prints:

promise-1

Inside it:

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

So macrotask queue becomes:

[ timeout-1, timeout-2 ]

Microtask 2: promise-2

Prints:

promise-2

Now microtasks are empty.

Run next macrotask

First macrotask is timeout-1.

It prints:

timeout-1

Then inside that timeout:

Promise.resolve().then(() => {
  console.log("promise-in-timeout");
});

This queues a microtask:

[ promise-in-timeout ]

Important rule

After a macrotask finishes, the runtime drains microtasks before taking the next macrotask.

So now it runs:

promise-in-timeout

Only then does it continue to next macrotask.

Next macrotask: timeout-2

Prints:

timeout-2

Final output:

start
end
promise-1
promise-2
timeout-1
promise-in-timeout
timeout-2

The pattern to use every time

When solving these, do this in order:

Step 1

Run all synchronous code first.

Step 2

While doing that, note:

  • promise handlers → microtask queue

  • setTimeout callbacks → macrotask/task queue

Step 3

After sync code ends:

  • drain all microtasks completely

Step 4

Run one macrotask

Step 5

Drain microtasks again

Step 6

Repeat


Tiny cheat rule

  • sync first

  • then all promises/microtasks

  • then one timeout/task

  • then microtasks again

  • repeat


One bonus trap question

console.log("A");

setTimeout(() => console.log("B"), 0);

Promise.resolve().then(() => {
  console.log("C");
  setTimeout(() => console.log("D"), 0);
});

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

console.log("F");

Try it yourself first.

Answer:

A
F
C
E
B
D

If you want, I’ll give you 3 even trickier ones involving async/await, because that is where most interview confusion starts.

Try this one:

console.log("A");

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

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

console.log("D");

What will be printed?

Options: A) A B C D B) A D B C C) A D C B D) C A D B

Correct answer: C) A D C B

Why:

  • console.log("A") runs first

  • setTimeout(..., 0) goes to the task/callback queue

  • Promise.then(...) goes to the microtask queue

  • console.log("D") runs next

  • after synchronous code finishes, microtasks run before timeouts

  • so "C" prints before "B"

Final output:

A
D
C
B

Here’s a slightly trickier one too:

console.log(1);

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

Promise.resolve().then(() => {
  console.log(3);
  setTimeout(() => console.log(4), 0);
});

Promise.resolve().then(() => console.log(5));

console.log(6);

Output:

1
6
3
5
2
4

Rule to remember:

  • sync code first

  • then promise microtasks

  • then setTimeout callbacks

Good. async/await is where event-loop questions start feeling sneaky, because await is basically promise scheduling in nicer syntax.

Here are 3, from medium to harder.

1) await splits execution

console.log("A");

async function test() {
  console.log("B");
  await Promise.resolve();
  console.log("C");
}

test();

console.log("D");

Output

A
B
D
C

Why

Line 1

console.log("A");

Prints A

Function definition

async function test() { ... }

Just defines the function.

Call test()

test();

The function starts running synchronously until the first await.

Inside it:

console.log("B");

Prints B

Then:

await Promise.resolve();

This does not continue immediately in the same sync flow.

It says:

  • resolve this promise

  • resume the rest of the function later as a microtask

So console.log("C") is deferred.

Next line

console.log("D");

Still part of current sync code, so it runs before the resumed async part.

Prints D

After sync code ends

The continuation after await runs as a microtask:

console.log("C");

Prints C


2) await vs normal promise .then

console.log(1);

async function foo() {
  console.log(2);
  await Promise.resolve();
  console.log(3);
}

foo();

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

console.log(5);

Output

1
2
5
3
4

Why

console.log(1)

Prints 1

foo()

Starts immediately.

Inside foo:

console.log(2);

Prints 2

Then:

await Promise.resolve();

This pauses foo and schedules the rest:

console.log(3);

as a microtask.

Microtask queue now:

[ resume foo -> print 3 ]

Then this line runs

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

This queues another microtask:

Microtask queue now:

[
  resume foo -> print 3,
  print 4
]

console.log(5)

Prints 5

Sync code done

Now drain microtasks in order queued:

  1. resume foo → print 3

  2. run .then callback → print 4

Final output:

1
2
5
3
4

Key lesson

The code after await behaves like a queued microtask, and its position depends on when it got queued.


3) Harder: await inside nested async flow

console.log("start");

async function one() {
  console.log("one-1");
  await Promise.resolve();
  console.log("one-2");
}

async function two() {
  console.log("two-1");
  await one();
  console.log("two-2");
}

two();

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

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

console.log("end");

Output

start
two-1
one-1
end
one-2
promise
two-2
timeout

Why

This one is subtle.


Line 1

console.log("start");

Prints start

Define functions

Nothing runs yet.

Call two()

two();

two() starts synchronously until its first await.

Inside two:

console.log("two-1");

Prints two-1

Then:

await one();

To evaluate that, one() is called first.


Inside one()

one() also starts synchronously until its first await.

console.log("one-1");

Prints one-1

Then:

await Promise.resolve();

This pauses one() and queues its continuation:

console.log("one-2");

as a microtask.

So now:

  • one() returns a pending promise

  • two() is waiting for that promise to finish

  • two() cannot continue to two-2 yet

Microtask queue:

[ resume one -> print one-2 ]

Next top-level promise

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

Queues another microtask.

Microtask queue:

[
  resume one -> print one-2,
  print promise
]

Timeout

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

Queues a macrotask for later.


console.log("end")

Prints end

So sync output so far:

start
two-1
one-1
end

Now drain microtasks

Microtask 1: resume one

Runs:

console.log("one-2");

Prints one-2

Now one() completes, which resolves the promise that two() was awaiting.

That means the continuation of two() gets queued as a new microtask:

console.log("two-2");

Microtask queue becomes:

[
  print promise,
  resume two -> print two-2
]

Microtask 2

Prints:

promise

Microtask 3

Resumes two():

console.log("two-2");

Prints two-2


Then macrotask

Timeout runs:

timeout

Final output:

start
two-1
one-1
end
one-2
promise
two-2
timeout

The big async/await rule

async/await is not magic. Think of it like:

await something

means:

  1. evaluate something

  2. wait for its promise

  3. resume the rest of the function later as a microtask

So:

async function f() {
  console.log("A");
  await x;
  console.log("B");
}

acts like:

  • print A now

  • schedule B for later after the awaited promise settles


Very important subtlety

Even if the promise is already resolved:

await Promise.resolve();

the code after await still does not run immediately in the current sync stack.

It still resumes asynchronously.

That is why:

async function f() {
  await Promise.resolve();
  console.log("after");
}

prints "after" later, not inline.


Tiny cheat sheet for async/await

  • code before first await runs synchronously

  • await pauses the function

  • code after await resumes in a microtask

  • setTimeout still waits for macrotask turn

  • promise continuations and await continuations both use microtask-style scheduling


One bonus question

Try this first:

console.log("A");

async function foo() {
  console.log("B");
  await null;
  console.log("C");
  await null;
  console.log("D");
}

foo();

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

console.log("F");

Answer

A
B
F
C
E
D

Why

  • A, B, F are sync

  • first await null pauses foo and queues continuation for C

  • promise queues E

  • first microtask resumes foo, prints C, hits second await, queues continuation for D

  • next microtask prints E

  • next microtask prints D

If you want, next I’ll give you 3 async/await questions involving return, throw, and try/catch/finally, which are also very common.

If you want, I can give you 5 interview-style promise/setTimeout questions with answers.

explain in detail iwth how macro micro threads come in picture and why is the all of this flow like this

Yes — this topic makes much more sense once you separate:

  1. call stack

  2. Web APIs / host environment

  3. task queues

  4. event loop

  5. microtasks vs macrotasks

  6. single JS thread

I’ll build it from the ground up.


1) JavaScript itself runs on a single main thread

In normal browser JavaScript, your JS code runs on one main thread.

That means only one piece of JS runs at a time.

So this code:

console.log("A");
console.log("B");
console.log("C");

runs in order, one line at a time.

JavaScript does not run these lines in parallel.

So the first important idea is:

  • JS execution is single-threaded

  • but the environment around JS can do async work

That surrounding environment is usually:

  • the browser

  • or Node.js


2) Then how can setTimeout and promises feel asynchronous?

Because JavaScript is not doing everything by itself.

When you write async code, the browser or Node provides mechanisms outside the main JS call stack.

Example:

setTimeout(() => {
  console.log("B");
}, 1000);

JavaScript does not sit there counting 1000 ms inside the call stack.

Instead:

  • JS asks the host environment to start a timer

  • JS continues running other code

  • when the timer completes, the callback is queued

  • later the event loop lets JS run that callback

So async behavior is a cooperation between:

  • the JS engine

  • the host environment

  • queues

  • the event loop


3) The call stack: where synchronous code runs

The call stack is where currently executing JS functions live.

Example:

function one() {
  two();
}

function two() {
  console.log("hello");
}

one();

Roughly:

  • one() pushed onto stack

  • inside it, two() pushed onto stack

  • console.log runs

  • two() pops off

  • one() pops off

The stack must be empty before the event loop can take new queued work and run it.

This is a huge rule:

Queued async callbacks do not run until the current synchronous stack is done.


4) What happens with setTimeout

Take this:

console.log("A");

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

console.log("C");

You might think 0 means immediate. It does not.

What really happens:

Step 1

console.log("A") runs immediately.

Output:

A

Step 2

setTimeout(...) is encountered.

JS registers the timer with the browser/Node timer system.

The callback does not run now.

It is handed off to the environment.

Step 3

console.log("C") runs.

Output:

A
C

Step 4

The synchronous code finishes. Stack becomes empty.

Step 5

The timer has expired, so its callback is placed in a task queue.

Step 6

The event loop sees the stack is empty and takes that callback from the queue and runs it.

Output:

A
C
B

So setTimeout(..., 0) means:

  • run it after current code finishes

  • and after appropriate queue handling

  • not literally instantly


5) What is the event loop?

The event loop is the mechanism that keeps checking:

  • Is the call stack empty?

  • Are there pending microtasks?

  • Are there pending tasks/macrotasks?

Then it decides what to run next.

A simplified model is:

  1. run synchronous code

  2. when stack is empty, run all microtasks

  3. then run one macrotask/task

  4. after that, run microtasks again

  5. continue

That is why promises often run before setTimeout.


6) Microtasks vs macrotasks

This is the part interview questions target.

Macrotasks / tasks

These include things like:

  • setTimeout

  • setInterval

  • DOM events like click handlers

  • message events

  • I/O callbacks in host environments

Microtasks

These include:

  • Promise.then

  • Promise.catch

  • Promise.finally

  • queueMicrotask

  • MutationObserver callbacks in browsers

Microtasks have higher priority than macrotasks.

So when current synchronous code ends:

  • all pending microtasks run first

  • only after that does the event loop move to the next macrotask


7) Why promises run before setTimeout

Take:

console.log("A");

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

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

console.log("D");

Let’s trace it carefully.

Synchronous phase

Line 1

console.log("A");

Prints:

A

Line 2

setTimeout(() => {
  console.log("B");
}, 0);
  • timer registered with host

  • callback will later enter macrotask queue

Nothing printed yet.

Line 3

Promise.resolve().then(() => {
  console.log("C");
});
  • promise already resolved

  • .then(...) callback goes into microtask queue

Nothing printed yet.

Line 4

console.log("D");

Prints:

D

So after sync code, output is:

A
D

Now synchronous stack is empty

Event loop checks microtasks first.

Microtask queue contains callback for "C".

So it runs:

C

Then it moves to macrotask queue

Now timeout callback runs:

B

Final output:

A
D
C
B

8) Why are microtasks given priority?

Because they are meant for very small follow-up jobs that should happen immediately after the current operation completes, before the environment moves on to other bigger tasks.

Promises are used a lot for:

  • continuation logic

  • chaining

  • cleanup

  • state transitions

So the language gives them a tighter scheduling guarantee.

This helps make async code more predictable.

For example, promise callbacks should run:

  • not immediately in the middle of current sync code

  • but as soon as possible after it

That is exactly what microtasks provide.


9) Why not just run promise callbacks immediately?

Because that would break consistency and lead to weird re-entrancy problems.

Example:

let done = false;

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

done = true;

If .then(...) ran immediately, it would print false.

But JS guarantees promise handlers run asynchronously, after current synchronous code, so it prints:

true

This is much easier to reason about.

So promise handlers are:

  • deferred

  • but high-priority deferred

That is why microtasks exist.


10) Why not put promises in the same queue as timeouts?

Because timeouts are broader host-scheduled tasks, while promises are language-level continuation jobs.

A timeout means:

  • “do this later”

A promise callback means:

  • “the current async step is complete; continue the chain right away”

Those are not the same kind of thing.

If promises waited behind normal tasks like timers and UI events, async flows would feel sluggish and less deterministic.


11) What does “run all microtasks” mean?

This is very important.

After one synchronous block or one macrotask completes, the runtime drains the microtask queue completely before moving on.

Example:

console.log("A");

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

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

Trace:

  • A sync → prints A

  • first promise callback queued as microtask

  • timeout callback queued as macrotask

  • sync code ends

  • run microtask: prints B

  • during that microtask, another promise callback is queued

  • before going to macrotask, runtime keeps draining microtasks

  • prints C

  • only then timeout runs → D

Output:

A
B
C
D

So newly added microtasks also run before the next macrotask.


12) This is why microtasks can starve macrotasks

Because if microtasks keep scheduling more microtasks, macrotasks may be delayed.

Example:

function loop() {
  Promise.resolve().then(loop);
}

loop();

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

The timeout may never get a chance, because the engine keeps draining microtasks forever.

That is one reason microtasks should stay short.


13) Where do threads come into the picture?

This is where people get confused.

JavaScript thread

Your JS code usually runs on one main thread.

Browser / host threads

The browser itself is multithreaded internally. It can have separate systems/threads for:

  • timers

  • networking

  • rendering

  • input events

  • background work

Similarly, Node has libuv and underlying system facilities for async I/O and timers.

So when people say “JS is single-threaded,” they mean:

  • your JS execution thread is single-threaded

They do not mean:

  • the entire browser or runtime has only one thread

The host can do work elsewhere, then queue callbacks back to JS.

That is the trick.


14) Example with fetch

console.log("start");

fetch("/data")
  .then(res => res.json())
  .then(data => console.log(data));

console.log("end");

What happens:

Sync phase

  • print "start"

  • fetch starts network request via browser/host

  • promise returned immediately

  • handlers are registered

  • print "end"

So far:

start
end

Later

  • network completes outside main JS execution

  • promise resolves

  • .then(...) callback gets queued as microtask

  • event loop runs it when current stack is empty

So async I/O is not blocking main JS.


15) Why does this architecture exist?

Because the web needs responsiveness.

Imagine JS blocked for every timer, network request, or click event. The page would freeze constantly.

Instead:

  • JS runs quickly on the main thread

  • long waiting operations happen outside it

  • callbacks come back later

This allows:

  • responsive UI

  • non-blocking I/O

  • ordered continuation logic


16) A full example

Consider:

console.log(1);

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

Promise.resolve().then(() => {
  console.log(3);
  setTimeout(() => console.log(4), 0);
});

Promise.resolve().then(() => console.log(5));

console.log(6);

Let’s trace exactly.

Synchronous code first

console.log(1)

Prints:

1

setTimeout(() => console.log(2), 0)

  • timeout callback scheduled as future macrotask

first promise

Promise.resolve().then(() => {
  console.log(3);
  setTimeout(() => console.log(4), 0);
});
  • callback queued as microtask

second promise

Promise.resolve().then(() => console.log(5));
  • callback queued as microtask

console.log(6)

Prints:

6

So after synchronous phase:

1
6

Microtask queue contains:

  • callback that prints 3 and schedules timeout 4

  • callback that prints 5

Macrotask queue has timeout 2 pending for later.

Drain microtasks

first microtask

Prints:

3

Inside it:

setTimeout(() => console.log(4), 0);

This schedules another macrotask.

second microtask

Prints:

5

Now microtasks are empty.

Run macrotasks

First timeout:

2

Then next timeout:

4

Final output:

1
6
3
5
2
4

17) A clean mental model

Think of it like this:

Call stack

“What is JS executing right now?”

Web APIs / host features

“Who handles timers, network, DOM events, etc. while JS is busy?”

Microtask queue

“Very urgent follow-up jobs, especially promise reactions.”

Macrotask queue

“Regular async callbacks like timers and events.”

Event loop

“The traffic controller that decides what runs next.”


18) Practical order to remember

Very simplified:

  1. run all synchronous code

  2. run all microtasks

  3. run one macrotask

  4. run all microtasks

  5. repeat

That one rule explains most interview questions.


19) Truth about “0 ms timeout”

setTimeout(fn, 0) does not mean:

  • run now

It means:

  • run after current code

  • after microtasks

  • when the event loop gets to that task

  • subject to minimum delay and runtime scheduling

So it is best thought of as: “schedule for later”


20) Why React and frameworks care about this

Because frameworks rely heavily on:

  • promise jobs

  • batching

  • scheduling

  • DOM rendering timing

Understanding microtasks helps explain:

  • why state updates may appear delayed

  • why promise-based code runs before timers

  • why some logs surprise you


21) The safest interview answer

If someone asks you to explain event loop behavior, say:

JavaScript runs synchronous code on a single call stack. Async operations like timers and network requests are handled by the host environment. When they are ready, their callbacks are queued. Promise handlers go into the microtask queue, and timers go into the macrotask queue. After the current synchronous code finishes, the event loop drains microtasks before taking the next macrotask. That is why Promise.then usually runs before setTimeout(..., 0).

That is the core.


22) One final compact example

console.log("start");

setTimeout(() => console.log("timeout"), 0);

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

console.log("end");

Output:

start
end
promise
timeout

Because:

  • sync first: start, end

  • microtasks next: promise

  • macrotasks after: timeout


23) Best rules to memorize

  • JS execution is single-threaded

  • host environment handles async waiting

  • setTimeout callbacks are macrotasks

  • Promise.then/catch/finally callbacks are microtasks

  • microtasks run before macrotasks

  • current sync code always finishes first

  • setTimeout(..., 0) is never truly immediate

If you want, I can next give you: 3 progressively harder event-loop questions and trace each one line by line.'

aise hi

CPU / Runtime level view
------------------------

        +--------------------------- Computer ---------------------------+
        |                                                               |
        |   CPU Core 1                  CPU Core 2                      |
        |   ---------                  ---------                        |
        |   runs threads               runs threads                     |
        |                                                               |
        +---------------------------------------------------------------+


Browser / Node Process
----------------------

+--------------------------- One Process -------------------------------+
|                                                                       |
|   Main JS Thread                                                      |
|   ----------------                                                    |
|   Registers  -> current instruction state                             |
|   Stack      -> current function calls                                |
|   Heap refs  -> points to JS objects in heap                          |
|                                                                       |
|   Example stack while running:                                        |
|                                                                       |
|      top ->  c()                                                      |
|              b()                                                      |
|              a()                                                      |
|              global                                                   |
|                                                                       |
|                                                                       |
|   Shared Heap Memory                                                  |
|   ------------------                                                  |
|      { user: {...} }                                                  |
|      { items: [...] }                                                 |
|      functions, closures, objects                                     |
|                                                                       |
|                                                                       |
|   Host Environment / Runtime Systems                                  |
|   ----------------------------------                                  |
|      Timers                                                           |
|      Network / fetch                                                  |
|      DOM events / I/O                                                 |
|      Rendering pipeline                                               |
|                                                                       |
|                                                                       |
|   Queues                                                              |
|   ------                                                              |
|      Microtask Queue: Promise.then, queueMicrotask                    |
|      Task Queue:      setTimeout, events, I/O callbacks               |
|                                                                       |
|                                                                       |
|   Event Loop                                                          |
|   ----------                                                          |
|      1. Wait until stack is empty                                     |
|      2. Drain microtasks                                              |
|      3. Run one task/macrotask                                        |
|      4. Drain microtasks again                                        |
|      5. Repeat                                                        |
|                                                                       |
+-----------------------------------------------------------------------+

Now map that to what “single-threaded JS” means.

1) One main JS thread

Your normal JS code runs on one main thread.

So at a given instant, the engine is doing one thing like:

  • evaluating an expression

  • calling a function

  • returning from a function

  • updating a variable

Not two JS call stacks at once on the same main thread.

Example:

function a() {
  b();
}
function b() {
  console.log("hi");
}
a();

Execution goes like this:

global
  -> a()
      -> b()
          -> console.log()

That is one path of execution.


2) Stack vs heap

Stack

The stack belongs to the thread and holds function call frames.

Each frame contains things like:

  • where to return to

  • local variables

  • arguments

  • bookkeeping for that call

Heap

The heap stores objects, arrays, closures, etc.

Example:

let obj = { x: 10 };

obj is a reference, and the actual object lives in heap memory.

So a thread’s stack often contains references to heap objects.

You can think of it like:

Stack frame:
  obj  ----->  Heap object { x: 10 }

3) Why single-threaded helps

If two JS threads could both mutate the same heap objects at the same time, you would get race conditions.

Example idea:

counter = counter + 1;

This is not one indivisible magical step. It is more like:

  1. read counter

  2. add 1

  3. write result

If two threads do that at once, updates can be lost.

That would force ordinary JS developers to think about:

  • locks

  • mutexes

  • atomics

  • deadlocks

  • memory visibility

JavaScript avoided that for the normal web programming model.


4) Then how do async things happen?

Because the host environment does work outside the main JS call stack.

For example:

setTimeout(() => console.log("timer"), 1000);

The main JS thread does not count 1000 ms itself.

Instead:

  • JS tells the host environment to start a timer

  • JS continues

  • later the host queues the callback

  • event loop runs it when the stack is free

Same with fetch:

  • browser/network layer handles request

  • JS thread is free meanwhile

  • response completion queues follow-up work later

So:

  • JS thread executes code

  • host environment waits/manages external async work


5) Diagram with example flow

Code:

console.log("A");

setTimeout(() => console.log("B"), 0);

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

console.log("D");

Flow:

Step 1: Main JS thread runs sync code

Stack:
  global -> console.log("A")    prints A

Stack:
  global -> setTimeout(...)     host registers timer

Stack:
  global -> Promise.then(...)   microtask queued

Stack:
  global -> console.log("D")    prints D

Sync code done
Stack empty


Step 2: Event loop checks microtasks first
Microtask queue: [ print C ]
Task queue:      [ print B ]

Run microtask -> prints C


Step 3: Event loop takes next task
Run timeout callback -> prints B

Output:

A
D
C
B

6) Where CPU cores fit

A CPU may have many cores.

That does not mean your JS code on the main thread is suddenly running in parallel across all of them.

It means the runtime/browser/OS can use other cores for:

  • rendering work

  • networking

  • decoding

  • worker threads

  • background tasks

But your normal main-thread JS still has one execution thread.

So:

  • the machine is multicore

  • the browser/runtime is often multithreaded

  • main-thread JS is still single-threaded

These are different statements.


7) What freezes the UI

This:

while (true) {}

blocks the main JS thread forever.

That means:

  • no other JS callbacks can run

  • event loop cannot hand JS new work

  • clicks wait

  • timers wait

  • page responsiveness breaks

The CPU is busy, but busy doing only your loop on that thread.


8) What about workers

With Web Workers or Worker Threads, JavaScript can run on additional threads.

So then you can have:

Main JS Thread      -> UI logic
Worker Thread       -> heavy computation

But the normal model stays safe because they usually communicate by:

  • messages

  • copied data

  • or carefully controlled shared memory APIs

Not by both casually mutating the DOM together.


9) Best mental picture

Think of it like this:

CPU
 └── can run many threads

Process (browser tab / Node app)
 ├── Main JS thread
 │    ├── stack
 │    └── executes JS one thing at a time
 │
 ├── Heap memory
 │    └── objects / arrays / closures
 │
 ├── Host systems
 │    ├── timers
 │    ├── network
 │    ├── events
 │    └── rendering / I/O
 │
 └── Event loop
      ├── microtasks
      └── tasks/macrotasks

10) One-line summary

“JavaScript is single-threaded” means that your normal JS code runs on one main execution thread with one call stack, while the runtime handles waiting and external async work outside that thread and schedules callbacks back through the event loop.

If you want, I can turn this into an even simpler office-workers analogy or a real CPU/register/stack-memory low-level version.