---
title: Javascript Visualized Promise Execution
description: JavaScript Promises visualized — microtask queue, task queue, event loop order. See exactly when each `.then()` fires with step-by-step diagrams and code.
url: https://nerdleveltech.com/javascript-visualized-promise-execution
language: en
date: '2023-01-15'
updated: '2026-03-27'
category: frontend
tags:
  - javascript
  - promises
  - async
  - recreated
markdown_url: https://nerdleveltech.com/javascript-visualized-promise-execution.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/javascript-visualized-promise-execution'
terms: https://nerdleveltech.com/llms.txt
---

# Javascript Visualized Promise Execution

## TL;DR

JavaScript Promises execute asynchronously via the microtask queue—each `.then()` handler runs after the current task completes but before the next macrotask, giving Promises priority over setTimeout and other I/O operations.

## Introduction

Promises are fundamental to modern JavaScript, yet their execution model confuses many developers. Why does a `.then()` handler run before a `setTimeout` callback, even if the timeout is 0ms? How does async/await differ from Promises under the hood? This post demystifies Promise execution by visualizing the event loop, microtask queue, and macrotask queue—the mechanisms that give Promises their power and behavior.

## The Event Loop: Macrotasks and Microtasks

JavaScript is single-threaded and uses an event loop to manage asynchronous operations. The loop processes two types of work:

**Macrotasks (Task Queue):** the initial script, setTimeout, setInterval, I/O, UI events
**Microtasks (Microtask Queue):** Promise reactions, `queueMicrotask()`, MutationObserver

Per the [HTML spec](https://html.spec.whatwg.org/multipage/webappapis.html#event-loops), each iteration of the event loop runs one task, drains the microtask queue completely, then performs any rendering update if there is a render opportunity, then loops. UI rendering only applies in browsers — Node.js has no rendering step.

### Visual: Event Loop Execution Order

```
┌─────────────────────────────────────────────────────┐
│ 1. Run one task                                     │
│    (initial script, setTimeout cb, I/O cb, etc.)    │
└─────────────────────────────────────────────────────┘
         ↓
┌─────────────────────────────────────────────────────┐
│ 2. Drain the microtask queue                        │
│    (Promise reactions, queueMicrotask)              │
│    Includes microtasks queued by other microtasks.  │
└─────────────────────────────────────────────────────┘
         ↓
┌─────────────────────────────────────────────────────┐
│ 3. Render (browsers only, if there's an opportunity)│
│    (style, layout, paint — may be skipped)          │
└─────────────────────────────────────────────────────┘
         ↓
│ [Loop back to step 1 with the next task]
```

The browser may skip rendering on a given iteration if nothing visual changed, or if it decides to coalesce frames. The microtask queue, however, is always drained to empty before the loop moves on.

### Code Example: Execution Order

```javascript
console.log('1. Sync start');

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

Promise.resolve()
  .then(() => console.log('3. Promise 1'))
  .then(() => console.log('4. Promise 2'));

queueMicrotask(() => console.log('5. queueMicrotask'));

console.log('6. Sync end');

// Output:
// 1. Sync start
// 6. Sync end
// 3. Promise 1
// 5. queueMicrotask
// 4. Promise 2
// 2. setTimeout

// Why? Synchronous code runs first, then microtasks (Promises, queueMicrotask),
// then macrotasks (setTimeout).
```

## Promise States and Transitions

A Promise starts **pending** and moves to either **fulfilled** or **rejected**, irreversibly.

```javascript
// Creating and resolving a Promise
const promise = new Promise((resolve, reject) => {
  console.log('A. Promise constructor runs immediately');

  setTimeout(() => {
    console.log('B. Inside setTimeout');
    resolve('success'); // Transition to fulfilled
  }, 0);
});

console.log('C. After Promise declaration');

promise.then(
  result => console.log('D. .then handler:', result),
  error => console.log('Error handler:', error)
);

console.log('E. After .then');

// Output:
// A. Promise constructor runs immediately
// C. After Promise declaration
// E. After .then
// B. Inside setTimeout
// D. .then handler: success

// Explanation:
// 1. Promise constructor (A) runs synchronously
// 2. setTimeout callback is scheduled as macrotask
// 3. .then is registered but doesn't execute immediately
// 4. After sync code (C, E), event loop processes microtasks
// 5. setTimeout executes (B), which resolves the Promise
// 6. Promise resolution queues the .then handler (D) as microtask
```

## Promise.then() Execution Flow

Each `.then()` call returns a new Promise. The handler's return value becomes the next Promise's fulfillment value.

```javascript
const p = Promise.resolve(1)
  .then(x => {
    console.log('Handler 1:', x);
    return x + 1; // New Promise resolves with 2
  })
  .then(x => {
    console.log('Handler 2:', x);
    return x + 1; // New Promise resolves with 3
  })
  .then(x => {
    console.log('Handler 3:', x);
  });

// Output:
// Handler 1: 1
// Handler 2: 2
// Handler 3: 3

// Visual: Promise Chain
//
// Promise { 1 }
//     ↓ .then(handler1)
// [queue handler1 as microtask]
//     ↓ (after sync code)
// Handler 1: 1 (returns 2)
//     ↓ (new Promise { 2 })
// [queue handler2 as microtask]
//     ↓ (after handler1 completes)
// Handler 2: 2 (returns 3)
//     ↓ (new Promise { 3 })
// [queue handler3 as microtask]
//     ↓ (after handler2 completes)
// Handler 3: 3
```

## Promise.withResolvers() (ES2024)

Before ES2024, creating a Promise with external resolution required storing `resolve` and `reject` references. `Promise.withResolvers()` simplifies this.

```javascript
// Traditional approach (verbose)
let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

// ES2024 approach (clean)
const { promise, resolve, reject } = Promise.withResolvers();

// Using it: external control
const { promise: p, resolve: r } = Promise.withResolvers();

document.getElementById('button').addEventListener('click', () => {
  r('Button clicked!');
});

const result = await p; // Waits for button click
console.log(result); // 'Button clicked!'

// Under the hood: Promise.withResolvers() is roughly equivalent to
// ┌──────────────────────────────────────────┐
// │ let resolve, reject;                     │
// │ const promise = new Promise((res, rej) =>│
// │   { resolve = res; reject = rej; });     │
// │ return { promise, resolve, reject };     │
// └──────────────────────────────────────────┘
//
// Browser support: all evergreen browsers since early 2024.
// Node.js: v22+ enables it by default (v21.7+ behind a flag).
```

## Async/Await Under the Hood

`async/await` is syntactic sugar over Promises and microtasks. An `async` function always returns a Promise, and every `await` pauses the function and resumes it as a microtask once the awaited value settles.

```javascript
// Async function
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

// Roughly equivalent Promise chain (rejections propagate through .then)
function fetchUserPromise(id) {
  return fetch(`/api/users/${id}`)
    .then(response => response.json());
}
// (The trailing `return user` simply forwards the value, so no extra .then is needed.)

// Execution order: await schedules continuation as a microtask
async function example() {
  console.log('1. Sync before await');

  const result = await Promise.resolve('async');
  console.log('2. After await:', result);

  return result;
}

example().then(() => console.log('3. After function'));

console.log('4. Sync after function call');

// Output:
// 1. Sync before await
// 4. Sync after function call
// 2. After await: async
// 3. After function
```

The synchronous prefix of an `async` function (everything before the first `await`) runs immediately when the function is called. At the first `await`, control returns to the caller — the rest of the function body resumes later as a microtask. V8 optimised this in 2018 so awaiting an already-resolved Promise costs a single microtask tick instead of three[^v8-fast-async].

---

## Related reads

- [Mastering JavaScript Async Patterns: From Callbacks to Concurrency](https://nerdleveltech.com/mastering-javascript-async-patterns-from-callbacks-to-concurrency)
- [Understand the Asynchronous Javascript Callbacks Promises and Async Await](https://nerdleveltech.com/understand-the-asynchronous-javascript-callbacks-promises-and-async-await)


[^v8-fast-async]: V8 team, ["Faster async functions and promises"](https://v8.dev/blog/fast-async).

## Error Handling in Promise Chains

Errors propagate through Promise chains, and `.catch()` handlers are also microtasks.

```javascript
Promise.resolve()
  .then(() => {
    console.log('Handler 1');
    throw new Error('Oops!');
  })
  .then(() => {
    console.log('Handler 2 (skipped)');
  })
  .catch(error => {
    console.log('Caught:', error.message);
    return 'recovered';
  })
  .then(result => {
    console.log('Handler 3:', result);
  });

// Output:
// Handler 1
// Caught: Oops!
// Handler 3: recovered

// Visualization: Error in Promise Chain
//
// Promise.resolve()
//     ↓
// [Handler 1 queued as microtask]
//     ↓ (execute)
// Handler 1 → throw Error
//     ↓ (state: rejected)
// [skip Handler 2]
//     ↓
// [.catch handler queued as microtask]
//     ↓ (execute)
// Caught: Oops! → return 'recovered'
//     ↓ (new Promise: fulfilled)
// [Handler 3 queued as microtask]
//     ↓ (execute)
// Handler 3: recovered
```

## Promise.allSettled() vs Promise.all()

`Promise.all()` rejects as soon as any input Promise rejects (fail-fast). `Promise.allSettled()` waits for all of them and reports each settlement.

```javascript
function makePromises() {
  return [
    Promise.resolve('a'),
    Promise.reject('b'),
    Promise.resolve('c')
  ];
}

// Promise.all: fails fast
Promise.all(makePromises()).catch(error => {
  console.log('Failed:', error); // 'b'
});

// Promise.allSettled: waits for all
Promise.allSettled(makePromises()).then(results => {
  console.log('Results:', results);
  // [
  //   { status: 'fulfilled', value: 'a' },
  //   { status: 'rejected', reason: 'b' },
  //   { status: 'fulfilled', value: 'c' }
  // ]
});
```

`Promise.race()` settles with the first input to settle (fulfilled or rejected). `Promise.any()` (ES2021) is the inverse of `Promise.all()` — it fulfils with the first fulfilled value and only rejects if every input rejects (with an `AggregateError`).

## Unhandled Promise Rejections

If a Promise rejects and no `.catch()` is attached, JavaScript emits an error event.

```javascript
// Unhandled rejection
Promise.reject('Oops!');

// Global handler (browser)
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled rejection:', event.reason);
  event.preventDefault(); // Prevent crash
});

// Global handler (Node.js)
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection:', reason);
});

// Attaching handler after delay avoids the error
setTimeout(() => {
  Promise.reject('Later error').catch(e => console.log(e));
}, 0);
```

## Practical Pattern: Retry with Exponential Backoff

Understanding microtasks and Promise execution enables complex patterns.

```javascript
async function retryWithBackoff(fn, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;

      const delay = Math.pow(2, attempt) * 100; // 100ms, 200ms, 400ms
      console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms`);

      // Wait using Promise (microtask) or setTimeout (macrotask)
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Usage
retryWithBackoff(async () => {
  const response = await fetch('/api/data');
  if (!response.ok) throw new Error('HTTP error');
  return response.json();
}).then(data => console.log('Success:', data));
```

## Conclusion

Promise execution is governed by the event loop's microtask queue. Every `.then()`, `.catch()`, and `.finally()` handler runs as a microtask, and the queue is drained completely between tasks — so a microtask queued during the current task always runs before the next `setTimeout` callback. `async`/`await` resumes through the same microtask machinery, so the same ordering applies. Master this model, and you'll understand race conditions, debug timing issues, and write reliable async code.
