Skip to main content
Knowledge Hub

How Promises Execute

Understanding promise lifecycle and execution in the event loop

Last updated: April 10, 2025

Promises are the foundation of modern JavaScript’s asynchronous system. Understanding how they work under the hood, how they execute in the event loop, and how they differ from other async mechanisms like setTimeout or callbacks is essential for writing predictable asynchronous code.

What Is a Promise?

A Promise in JavaScript is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Think of it as a placeholder for a value that isn’t available yet but will be fulfilled later.

Core States of a Promise

A Promise can be in one of three states:

StateDescriptionTrigger
PendingInitial state, not yet completeWhen created
FulfilledOperation completed successfullyresolve() called
RejectedOperation failedreject() called

Once a Promise becomes fulfilled or rejected, it’s considered settled and its state cannot change again.

Promise Structure

A Promise takes an executor function as its argument, which receives two callbacks: resolve() and reject().

const promise = new Promise((resolve, reject) => {
  // some async operation
  if (success) resolve("Done!");
  else reject("Error!");
});

The executor function runs immediately, synchronously when the Promise is created. But the result (then() or catch()) executes asynchronously, through the microtask queue.

Promise Lifecycle and Execution Flow

Creation

When you create a Promise, its executor runs synchronously:

console.log("1");

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

console.log("4");

Output:

1
2
4

resolve(“3”) only schedules the callback. It doesn’t execute it yet.

Resolution (Microtask Scheduling)

When resolve() or reject() is called:

  • The Promise moves to the fulfilled/rejected state
  • The callbacks attached via .then() or .catch() are placed into the microtask queue

So the executor runs immediately, but .then() callbacks run asynchronously, right after the current script finishes.

Callback Execution (via Event Loop)

When the call stack is empty, the event loop picks the microtasks in FIFO order.

This ensures all .then() handlers are executed before any macrotask (like setTimeout).

Example: Execution Order

console.log("A");

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

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

console.log("D");

Step-by-step

  1. “A” printed immediately
  2. setTimeout() scheduled (macrotask)
  3. Promise.resolve().then() microtask scheduled
  4. “D” printed immediately
  5. Call stack empty, event loop runs microtask queue first, printing “C”
  6. Then runs macrotask queue, printing “B”

Output:

A
D
C
B

The Promise Job Queue (Microtask Queue)

The Promise Job Queue, also called the microtask queue, is a specialized queue in the event loop dedicated to Promise callbacks.

Characteristics:

  • Executed immediately after the current stack, before any macrotask
  • Keeps the Promise chain consistent and predictable

Chaining Promises

Each .then() call returns a new Promise, enabling chaining:

Promise.resolve(1)
  .then(v => v + 1)
  .then(v => v * 2)
  .then(console.log);

Flow

  1. Promise.resolve(1) fulfilled immediately
  2. .then(v => v + 1) scheduled as a microtask
  3. When resolved, .then(v => v * 2) becomes another microtask
  4. Finally .then(console.log) prints 4

Output:

4

Each .then() callback runs asynchronously and creates a new microtask in the queue.

Important: Promises Are Always Asynchronous

Even if you resolve immediately, .then() will never run synchronously.

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

Output:

Sync
Async

Because .then() callbacks are microtasks, they wait for the current script to finish.

Promise Error Handling

Using .catch()

Promise.reject("Error!")
  .then(() => console.log("Success"))
  .catch(err => console.log("Caught:", err));

Output:

Caught: Error!

Using try…catch inside async/await

async function run() {
  try {
    await Promise.reject("Fail!");
  } catch (e) {
    console.log("Caught:", e);
  }
}
run();

Deep Execution Flow Example

console.log("1");

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

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

console.log("5");

Execution Steps

  1. “1” printed
  2. setTimeout() scheduled (macrotask)
  3. Promise.resolve() schedules microtasks
  4. “5” printed
  5. Stack empty, event loop runs microtasks:
    • “3”
    • “4”
  6. After microtasks, macrotask “2” runs

Output:

1
5
3
4
2

Inside the Engine (Simplified Model)

┌──────────────────────────────┐
│  JavaScript Engine (V8)      │
│ ┌──────────────┐             │
│ │ Call Stack   │◄────────────┐
│ └──────────────┘             │
│ ┌──────────────┐             │
│ │ Microtask Q  │ (Promises)  │
│ └──────────────┘             │
│ ┌──────────────┐             │
│ │ Macrotask Q  │ (Timers, IO)│
│ └──────────────┘             │
└───────────────┬──────────────┘


        ┌────────────────┐
        │   Event Loop   │
        └────────────────┘
  • Promise .then() callbacks go to the Microtask Queue
  • setTimeout goes to the Macrotask Queue
  • Event loop executes all microtasks first, then one macrotask, then repeats

Async/Await: Syntactic Sugar for Promises

async function example() {
  console.log("Start");
  await Promise.resolve();
  console.log("End");
}
example();
console.log("After");

Output:

Start
After
End

Explanation:

  • The await pauses only that async function, not the entire thread
  • The code after await runs as a microtask

Summary

ConceptDescription
PromiseObject representing future completion of async operation
StatesPending → Fulfilled or Rejected
ExecutorRuns synchronously when Promise created
.then() / .catch()Scheduled as microtasks
Microtask QueueExecutes before macrotasks
ChainingEach .then() returns a new Promise
Async/AwaitCleaner syntax using Promises internally

Summary Flow

Promise created → Executor runs immediately

resolve()/reject() called → Handlers pushed to Microtask Queue

Call stack empty → Event Loop processes Microtasks

.then() / .catch() executed

Macrotasks (like setTimeout) run afterward

Understanding how promises execute is crucial for debugging asynchronous code. The microtask queue ensures promise callbacks run before other async operations, providing predictable execution order for chained async operations.