Asynchronous JavaScript:

Vishal PandeyVishal Pandey
9 min read

JavaScript Runtime Environment

Synchronous Javascript Execution:

Code:

javascriptCopyEditconsole.log("start");

const promise = new Promise((resolve, reject)=>{
  console.log("1");
  resolve("2");
})

promise.then((res)=>{
  console.log(res);
})

console.log("end");

πŸ”„ Execution Flow with JS Runtime:

1. console.log("start")

  • Goes to call stack.

  • Prints: start

  • Gets popped off.

2. Creating the Promise

jsCopyEditconst promise = new Promise((resolve, reject)=>{
  console.log("1");
  resolve("2");
})
  • Constructor function passed to Promise executes immediately.

  • console.log("1") goes to call stack β†’ prints: 1.

  • resolve("2") is called β†’ the value "2" is stored, and then() callback is queued in the microtask queue.

3. promise.then(...)

jsCopyEditpromise.then((res)=>{
  console.log(res);
})
  • Registers a .then() callback to be executed when the promise resolves.

  • Since the promise is already resolved, this callback is put into the microtask queue.

4. console.log("end")

  • Goes to call stack β†’ prints: end.

πŸ“¦ Now the Call Stack is empty.


🧠 Microtask Queue:

Now the event loop checks the microtask queue:

  • It finds the .then() callback:
jsCopyEdit(res) => { console.log(res); }
  • This goes to call stack.

  • console.log("2") executes β†’ prints: 2

  • Gets popped off.


βœ… Final Output in Console:

sqlCopyEditstart
1
end
2

Asynchronous JavaScript Execution with Promises:

Let's break down how JavaScript executes these promises with their timeouts, explaining the call stack, event loop, and microtask queue.

const p1 = new Promise((resolve) => {
  setTimeout(() => resolve('p1 resolved'), 10000); // 10 seconds
});

const p2 = new Promise((resolve) => {
  setTimeout(() => resolve('p2 resolved'), 5000); // 5 seconds
});

function runPromises() {
  p1.then(result => console.log(result));
  p2.then(result => console.log(result));
}

runPromises();

Execution Flow Step-by-Step

1. Initial Execution Phase

  • Call Stack:[main context]

  • Web APIs: None

  • Callback Queue: Empty

  • Microtask Queue: Empty

  1. p1 Promise is created:

    • The executor function runs immediately

    • setTimeout is called (Web API)

    • Timer (10s) starts in the background

    • Callback registered with Web APIs

  2. p2 Promise is created:

    • The executor function runs immediately

    • setTimeout is called (Web API)

    • Timer (5s) starts in the background

    • Callback registered with Web APIs

  3. runPromises() is called:

    • p1.then() registers a fulfillment handler (adds to microtask queue when resolved)

    • p2.then() registers a fulfillment handler (adds to microtask queue when resolved)

2. After 5 Seconds (p2 timeout completes)

  • Call Stack: [main context] (already empty, script finished)

  • Web APIs: p1's timer still running (5s remaining)

  • Callback Queue:[p2's setTimeout callback]

  • Microtask Queue: Empty

Event Loop Process:

  1. The p2 timeout callback moves from Web APIs to Callback Queue

  2. Event loop sees call stack is empty and moves callback to call stack

  3. Callback executes:resolve('p2 resolved')

    • This puts p2's .then() handler into the microtask queue
  4. Event loop prioritizes microtasks over other callbacks

  5. Microtask executes: console.log('p2 resolved') runs

3. After 10 Seconds (p1 timeout completes)

  • Call Stack: [main context] (empty)

  • Web APIs: All timers completed

  • Callback Queue:[p1's setTimeout callback]

  • Microtask Queue: Empty

Event Loop Process:

  1. The p1 timeout callback moves from Web APIs to Callback Queue

  2. Event loop moves it to call stack

  3. Callback executes:resolve('p1 resolved')

    • This puts p1's .then() handler into the microtask queue
  4. Microtask executes: console.log('p1 resolved') runs

Key Observations

  1. Order of Execution:

    • Even though we attached p1's handler first, p2 resolves first because its timeout is shorter

    • Output will be:

        p2 resolved
        p1 resolved
      
  2. Microtask Queue Priority:

    • Promise resolutions go to the microtask queue

    • The event loop processes all microtasks before moving to the next callback

  3. Non-blocking Nature:

    • The main thread isn't blocked during the 10-second wait

    • Other code could execute during this time (though in this example there isn't any)

Visualization of the Process:

Time 0s:
- p1: setTimeout(10s) registered
- p2: setTimeout(5s) registered
- Handlers attached via .then()

Time 5s:
- p2's timeout completes
- p2's resolve callback executes
- p2's .then() handler added to microtask queue
- microtask executes: console.log('p2 resolved')

Time 10s:
- p1's timeout completes
- p1's resolve callback executes
- p1's .then() handler added to microtask queue
- microtask executes: console.log('p1 resolved')

This demonstrates how JavaScript handles asynchronous operations without blocking the main thread, using the event loop and task queues.

Asynchronous Execution with async/await:

  • Async makes a function to return a promise.

  • Await makes an async function wait for a promise.

  • Allows writing asynchronous code in a synchronous manner.

  • Async function does not have resolve and reject parameter

  • Everything after the await keyword is placed in the event queue.

Let's analyze the same example but using async/await syntax, which provides a more synchronous-looking way to handle promises.

Modified Example with async/await:

const p1 = new Promise((resolve) => {
  setTimeout(() => resolve('p1 resolved'), 10000); // 10 seconds
});

const p2 = new Promise((resolve) => {
  setTimeout(() => resolve('p2 resolved'), 5000); // 5 seconds
});

async function runPromises() {
  console.log(await p1);
  console.log(await p2);
}

runPromises();

Key Differences from .then() Approach

  1. Execution Flow: With⁣await, the function pauses at each await until the promise settles

  2. Order Dependence: The promises are now processed sequentially rather than in parallel

  3. Microtask Behavior: Each await yields control back to the event loop

Execution Flow Step-by-Step

1. Initial Execution Phase

  • Call Stack: [main context]

  • Web APIs:

    • p1: 10s timer registered

    • p2: 5s timer registered

  • Callback Queue: Empty

  • Microtask Queue: Empty

2. runPromises() Execution (Time 0s)

  1. Function enters call stack

  2. Hits await p1:

    • The function pauses execution

    • Returns control to the event loop (call stack empties)

    • The rest of the function becomes a callback attached to p1's resolution

3. After 5 Seconds (p2 resolves)

  • Call Stack: Empty

  • Web APIs: p1's timer still running (5s remaining)

  • Callback Queue: [p2's setTimeout callback]

  • Microtask Queue: Empty

But nothing happens with p2's resolution because:

  • No .then() handlers are attached directly to p2

  • The await p2 statement hasn't been reached yet (it's after await p1)

4. After 10 Seconds (p1 resolves)

  • Call Stack: [p1's setTimeout callback]

  • Web APIs: All timers completed

  • Callback Queue: Empty

  • Microtask Queue: [continuation of runPromises]

Event Loop Process:

  1. p1's timeout callback executes resolve('p1 resolved')

  2. This resumes the runPromises function at the first await

  3. console.log('p1 resolved') executes

  4. Function proceeds to await p2:

    • Since p2 already resolved at 5s, its value is immediately available

    • console.log('p2 resolved') executes immediately

Final Output

p1 resolved
p2 resolved

Comparison with Original .then() Version

Aspect.then() Versionasync/await Version
Execution OrderParallel (race)Sequential
Output Orderp2, p1 (by timeout)p1, p2 (by await order)
TimingBoth resolve independentlyp2 waits for p1 to complete
Resource UsageMore efficientLess efficient

Important Notes

  1. Sequential Nature: Each await pauses the function until resolution

  2. Already-Resolved Promises: If you await a settled promise, it continues immediately

  3. Error Handling: You can use try/catch with async/await

More Efficient async/await Pattern

If you want parallel execution with async/await (like the original .then() behavior):

javascript

Copy

Download

async function runPromisesParallel() {
  const result1 = p1;  // Don't await yet
  const result2 = p2;  // Don't await yet

  console.log(await result1);  // Now await both
  console.log(await result2);
}
// Output order will depend on timeout durations

This version starts both promises immediately and awaits them later, similar to the original Promise.all behavior.

Q: I have a doubt that when p2's setTimeout callback was in the callback queue and p1's timer completed 10 sec, then p1's setTimeout callback should come in the callback queue, and now the callback queue should have p2's setTimeout callback and p1's setTimeout callback, and then it lets them execute one by one in the call stack, but you mentioned that once p1's timer completes, it directly goes to the call stack ?

You're absolutely right to question this! Let me clarify the exact sequence of events with more precision, because there was indeed an oversimplification in my previous explanation.

Corrected Detailed Execution Flow

Let's go through the timeline with proper attention to the callback queue mechanics:

Initial Setup (Time 0s)

  • Call Stack: [main context] β†’ executes all synchronous code

  • Web APIs:

    • p1: 10s timer registered

    • p2: 5s timer registered

  • Callback Queue: []

  • Microtask Queue: []

At 5s (p2's timer completes)

  1. Web API moves p2's callback to the callback queue.

    • Callback Queue: [p2TimeoutCallback]
  2. Event Loop checks:

    • Call stack is empty (script has finished initial execution)

    • Moves p2's callback to call stack

  3. Call stack executes p2TimeoutCallback:

    • Calls resolve('p2 resolved')

    • This adds p2's .then() handler to the Microtask Queue

    • But in our async/await example, we don't have explicit .then()!

  4. Since we're using await, the resolution is handled differently:

    • The promise resolution is noted internally

    • No microtask is queued yet because nothing is awaiting p2 at this point

At 10s (p1's timer completes)

  1. Web API moves p1's callback to Callback Queue:

    • Callback Queue: [p1TimeoutCallback]
  2. Event Loop:

    • Call stack is empty

    • Moves p1's callback to call stack

  3. Call stack executes p1TimeoutCallback:

    • Calls resolve('p1 resolved')

    • This resumes the paused runPromises() function:

      • The await p1 now completes

      • console.log('p1 resolved') executes

      • Then hits await p2

  4. Checking p2's state:

    • p2 is already resolved (since 5s)

    • Immediately continues to console.log('p2 resolved')

Key Clarifications

  1. Callback Queue Behavior:

    • Both timer callbacks did go through the callback queue

    • They were executed in order of timer completion (p2 first, then p1)

    • But in our async/await example, p2's resolution was "orphaned" because nothing was actively awaiting it when it resolved

  2. Critical Difference from .then():

     // .then() version
     p1.then(...) // Attaches handler immediately
     p2.then(...) // Attaches handler immediately
     // Both can trigger independently
    
     // async/await version
     await p1 // Blocks until p1 resolves
     await p2 // Only reached after p1 resolves
    
  3. What Really Happens with p2's Resolution:

    • At 5s: p2 resolves, but the await p2 statement hasn't been reached yet

    • The promise resolution is stored internally

    • When execution finally reaches await p2 at 10s+, it sees p2 is already resolved and continues immediately

Visual Timeline Correction

Time 0s:
- Timers started (p1:10s, p2:5s)
- runPromises() hits await p1 and pauses

Time 5s:
- p2's timeout completes
- Callback Queue: [p2TimeoutCallback]
- Event loop moves it to call stack
- p2 resolves ('p2 resolved')
- (No visible effect because nothing is awaiting yet)

Time 10s:
- p1's timeout completes
- Callback Queue: [p1TimeoutCallback]
- Event loop moves it to call stack
- p1 resolves ('p1 resolved')
- runPromises() resumes:
  - logs 'p1 resolved'
  - hits await p2 (already resolved)
  - immediately logs 'p2 resolved'

This explains why you get

p1 resolved
p2 resolved

Instead of the parallel version's:

p2 resolved
p1 resolved
0
Subscribe to my newsletter

Read articles from Vishal Pandey directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vishal Pandey
Vishal Pandey