Understanding the JavaScript Event Loop & Microtask Queue

Promises are one of the most fundamental concepts in modern asynchronous JavaScript. But what really happens under the hood when a Promise settles?

When you call new Promise(executor), JavaScript creates an object with internal slots [[PromiseState]] and [[PromiseResult]]. The state begins as pending, and transitions irreversibly to either fulfilled or rejected.

Microtasks vs Macrotasks

Promise reactions (.then(), .catch(), .finally()) are queued into the Microtask Queue, which takes priority over the standard Macrotask queue (such as setTimeout or DOM events) at the end of each tick.

// Example of microtask ordering
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Output: 1, 4, 3, 2

The Three Internal States

Every Promise starts as pending. It can then settle exactly once: either fulfilled with a result or rejected with a reason.

Think of a Promise as a receipt for a result that the engine will hand you later.

Creating and Consuming a Promise

The executor runs synchronously and immediately, while the callbacks passed to .then() and .catch() run asynchronously after the Promise settles via the microtask queue.

const profile = fetch("/api/profile")
  .then((response) => response.json())
  .then((user) => renderProfile(user))
  .catch((error) => showError(error));

Why Chaining Works

Each call to .then() returns a completely new Promise. Returning a value fulfills the next step; throwing an error sends control to the nearest .catch() handler. This keeps each async step decoupled without nesting callbacks.

Practical Production Tip

Always handle failure at the boundary of an async workflow. A visible, graceful error state is far better than an uncaught silent promise rejection.