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:
| State | Description | Trigger |
|---|---|---|
| Pending | Initial state, not yet complete | When created |
| Fulfilled | Operation completed successfully | resolve() called |
| Rejected | Operation failed | reject() 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
- “A” printed immediately
- setTimeout() scheduled (macrotask)
- Promise.resolve().then() microtask scheduled
- “D” printed immediately
- Call stack empty, event loop runs microtask queue first, printing “C”
- 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
- Promise.resolve(1) fulfilled immediately
- .then(v => v + 1) scheduled as a microtask
- When resolved, .then(v => v * 2) becomes another microtask
- 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” printed
- setTimeout() scheduled (macrotask)
- Promise.resolve() schedules microtasks
- “5” printed
- Stack empty, event loop runs microtasks:
- “3”
- “4”
- 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
| Concept | Description |
|---|---|
| Promise | Object representing future completion of async operation |
| States | Pending → Fulfilled or Rejected |
| Executor | Runs synchronously when Promise created |
| .then() / .catch() | Scheduled as microtasks |
| Microtask Queue | Executes before macrotasks |
| Chaining | Each .then() returns a new Promise |
| Async/Await | Cleaner 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.