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