Lydia Hallie
9 min video
3 min read
How JavaScript Promises Actually Work
You just saved 6 min.
The big takeaway
Promises use internal slots (state, result, reaction records) to manage asynchronous operations. When resolved, handlers are queued to the microtask queue, allowing non-blocking execution. Chaining then() methods creates new promises and enables incremental result handling.
Promise Object Internals
Promise Constructor Creates Object with Internal Slots
When new Promise() is called, a promise object is created in memory containing internal slots: promise state, promise result, promise fulfill reactions, promise reject reactions, and promise is handled. These slots store the promise's current status and associated handlers.
Resolve and Reject Change Promise State
Calling resolve() sets the promise state to fulfilled and stores the passed value in promise result. Calling reject() sets the state to rejected and stores the error value in promise result. Both are simple property changes on the promise object.
Before resolve/reject
state: pending, result: undefined
After resolve(value)
state: fulfilled, result: value
Promise state transitions
Promise Reaction Records and Then Handlers
Then and Catch Create Promise Reaction Records
Chaining .then() or .catch() to a promise creates a promise reaction record containing a handler (the callback function you passed). These records store the code that should run when the promise resolves or rejects.
Then Returns a New Promise
The then() method not only creates a reaction record but also returns a new promise object. This enables promise chaining, where each then() in the chain creates a new promise that resolves with the return value of the previous handler.
Asynchronous Execution and the Microtask Queue
Handlers Are Added to Microtask Queue, Not Called Immediately
When a promise resolves, its handler is not executed immediately. Instead, it is added to the microtask queue. The event loop processes the microtask queue only after the call stack is empty, enabling non-blocking execution while the script continues running.
1
resolve() called, promise state set to fulfilled
2
Handler added to microtask queue
3
Call stack empties
4
Event loop checks microtask queue
5
Handler moved to call stack and executed
Promise handler execution flow
Microtask Queue Has Priority Over Task Queue
When the call stack is empty, the event loop first checks and processes all tasks in the microtask queue before moving to the task queue (also called callback queue or macro task queue). This ensures promise handlers execute before timers and other macrotasks.
Synchronous vs Asynchronous Promise Resolution
Synchronous Resolution in Constructor
If resolve() is called directly in the executor function (synchronously), the promise state and result are set immediately. However, any then() handlers attached later are still queued to the microtask queue, not executed synchronously.
Asynchronous Tasks Delay Resolution
When the executor function contains asynchronous operations like setTimeout, fetch, or file system reads, the callback that calls resolve() is queued to the task queue. Once that callback executes and calls resolve(), the promise state changes and handlers are queued to the microtask queue.
Promise Chaining and Incremental Result Handling
Chaining Then Methods Transforms Values
Each then() handler receives the resolved value from the previous promise, can transform it (e.g., multiply by 2), and returns a new value. The next then() in the chain receives that transformed value. This allows incremental, non-blocking processing of promise results.
1
Promise resolves with value 1
2
First then: result * 2 = 2, returns new promise
3
Second then: result * 2 = 4, returns new promise
4
Third then: logs result (4)
Promise chain transformation example
Real-World Use Case: Image Processing Pipeline
Promise chaining enables sequential processing of data in a non-blocking way. For example, you could chain then() methods to resize an image, apply a filter, and change the format, with each step receiving the result of the previous step.
Execution Order Example: Synchronous and Asynchronous Mix
Console Log Order Depends on Call Stack and Queues
In a script mixing synchronous code and promise handlers, console.log statements execute in this order: (1) synchronous logs in the executor function, (2) synchronous logs after the promise constructor, (3) promise handlers from the microtask queue. This produces output like 1, 3, 2 when logs are at those positions.
1
console.log(1) in executor - call stack
2
resolve(2) called - promise state set to fulfilled
3
console.log(3) after constructor - call stack
4
Call stack empty, microtask queue processed
5
console.log(result) where result=2 - microtask queue
Execution order: 1, 3, 2
Worth quoting
"Once you understand what happens behind the scenes under the hood they're actually not that complicated"
— Lydia Hallie, at [0:05]
"The nice thing about the fact that it's added to the microtask queue is that in the meantime our script can just keep running"
— Lydia Hallie, at [4:30]
"We can handle the promise result in a nonblocking way"
— Lydia Hallie, at [4:36]
Made with Glimpse by Wozart
glimpse.wozart.com/v/j0kb6o0x
Share this infographic
Read this infographic as text

How JavaScript Promises Actually Work

Summary of the video “JavaScript Visualized - Promise Execution by Lydia Hallie.

Promises use internal slots (state, result, reaction records) to manage asynchronous operations. When resolved, handlers are queued to the microtask queue, allowing non-blocking execution. Chaining then() methods creates new promises and enables incremental result handling.

Promise Object Internals

Promise Constructor Creates Object with Internal Slots

When new Promise() is called, a promise object is created in memory containing internal slots: promise state, promise result, promise fulfill reactions, promise reject reactions, and promise is handled. These slots store the promise's current status and associated handlers.

Resolve and Reject Change Promise State

Calling resolve() sets the promise state to fulfilled and stores the passed value in promise result. Calling reject() sets the state to rejected and stores the error value in promise result. Both are simple property changes on the promise object.

Promise Reaction Records and Then Handlers

Then and Catch Create Promise Reaction Records

Chaining .then() or .catch() to a promise creates a promise reaction record containing a handler (the callback function you passed). These records store the code that should run when the promise resolves or rejects.

Then Returns a New Promise

The then() method not only creates a reaction record but also returns a new promise object. This enables promise chaining, where each then() in the chain creates a new promise that resolves with the return value of the previous handler.

Asynchronous Execution and the Microtask Queue

Handlers Are Added to Microtask Queue, Not Called Immediately

When a promise resolves, its handler is not executed immediately. Instead, it is added to the microtask queue. The event loop processes the microtask queue only after the call stack is empty, enabling non-blocking execution while the script continues running.

Microtask Queue Has Priority Over Task Queue

When the call stack is empty, the event loop first checks and processes all tasks in the microtask queue before moving to the task queue (also called callback queue or macro task queue). This ensures promise handlers execute before timers and other macrotasks.

Synchronous vs Asynchronous Promise Resolution

Synchronous Resolution in Constructor

If resolve() is called directly in the executor function (synchronously), the promise state and result are set immediately. However, any then() handlers attached later are still queued to the microtask queue, not executed synchronously.

Asynchronous Tasks Delay Resolution

When the executor function contains asynchronous operations like setTimeout, fetch, or file system reads, the callback that calls resolve() is queued to the task queue. Once that callback executes and calls resolve(), the promise state changes and handlers are queued to the microtask queue.

Promise Chaining and Incremental Result Handling

Chaining Then Methods Transforms Values

Each then() handler receives the resolved value from the previous promise, can transform it (e.g., multiply by 2), and returns a new value. The next then() in the chain receives that transformed value. This allows incremental, non-blocking processing of promise results.

Real-World Use Case: Image Processing Pipeline

Promise chaining enables sequential processing of data in a non-blocking way. For example, you could chain then() methods to resize an image, apply a filter, and change the format, with each step receiving the result of the previous step.

Execution Order Example: Synchronous and Asynchronous Mix

Console Log Order Depends on Call Stack and Queues

In a script mixing synchronous code and promise handlers, console.log statements execute in this order: (1) synchronous logs in the executor function, (2) synchronous logs after the promise constructor, (3) promise handlers from the microtask queue. This produces output like 1, 3, 2 when logs are at those positions.

Notable quotes

Once you understand what happens behind the scenes under the hood they're actually not that complicated — Lydia Hallie
The nice thing about the fact that it's added to the microtask queue is that in the meantime our script can just keep running — Lydia Hallie
We can handle the promise result in a nonblocking way — Lydia Hallie

More like this