JavaScript Promises
Learn JavaScript Promises: the three states, executor function with resolve/reject, then/catch/finally, and microtask timing.
Promises in JavaScript are a powerful tool for managing asynchronous operations, enabling developers to write cleaner, more robust code. This chapter explains what a promise is, the three states it can be in, how the executor function works with resolve and reject, how to consume results with .then, .catch, and .finally, and when promise callbacks actually run (the microtask queue). Understanding these fundamentals is essential before moving on to chaining, the Promise API, and async/await.
Introduction to JavaScript Promises
A Promise is an object that represents a value that may not be available yet, but will be at some point in the future. Instead of passing a callback into an asynchronous function and hoping it gets called, you receive a promise object immediately and attach callbacks to it. This avoids deeply nested callbacks, often called "callback hell," and gives you a single, consistent way to handle both success and failure.
A typical asynchronous task — a network request, a timer, reading a file — does not have its result ready right away. The promise is a placeholder for that result.
The Three States of a Promise
A promise is always in exactly one of three states:
- pending — the initial state; the operation has not completed yet.
- fulfilled — the operation completed successfully, and the promise has a value.
- rejected — the operation failed, and the promise has a reason (usually an
Error).
A pending promise can transition to either fulfilled or rejected. Once it does, it is settled and can never change state again. This one-way, one-time transition is what makes promises predictable: a .then callback attached to an already-fulfilled promise will still run, and a promise can never flip from fulfilled back to rejected.
┌─────────────┐ resolve(value) ┌─────────────┐
│ pending │ ────────────────▶ │ fulfilled │
new Promise ─▶ │ │ └─────────────┘
│ │ reject(reason) ┌─────────────┐
└─────────────┘ ────────────────▶ │ rejected │
└─────────────┘Creating a Promise
To create a promise, you call the Promise constructor and pass it a function called the executor. The executor runs immediately and synchronously the moment the promise is created. It receives two functions as arguments, conventionally named resolve and reject:
- Call
resolve(value)to fulfill the promise withvalue. - Call
reject(reason)to reject the promise withreason.
Until you call one of them, the promise stays pending.
The executor receives resolve and reject so it can settle the promise once the asynchronous work finishes. Here the promise becomes fulfilled after a one-second timer:
Note: Only the first call to
resolveorrejectmatters. Once a promise is settled, any further calls toresolveorrejectare ignored. Also, if the executor throws an error synchronously, the promise is automatically rejected with that error.
Handling Outcomes with .then, .catch, and .finally
Once a promise has been created, you consume its result with the .then, .catch, and .finally methods. These are how the rest of your code reacts to a settled promise.
The then method
The .then method is used to schedule a callback to be executed when the promise is fulfilled. In order for a promise to be fulfilled, the resolve method should be called. The argument you pass to the resolve method will be the final value for the promise.
In this code, the promise will be fulfilled only after the 1000 ms timeout is done, and the resolve method is called with "Done!".
The function in the then part is only executed after the resolve method is called.
.then can also take a second argument — a rejection handler — but using a separate .catch (below) is clearer and catches errors from earlier handlers too.
The .catch method
The .catch method is used to handle the promise if it gets rejected. It means either an error is thrown in the promise function block or the reject method is called.
For richer error-handling patterns — rejection vs. thrown errors, re-throwing, and recovering inside a chain — see Error Handling with Promises.
The .finally method
The .finally method allows you to execute code after the promise is settled, regardless of its outcome. It receives no arguments (it does not know whether the promise fulfilled or rejected) and passes the result or error through unchanged, so it is ideal for cleanup such as hiding a loading spinner.
When Do Promise Callbacks Run? (Microtasks)
A common surprise is that .then, .catch, and .finally callbacks never run synchronously, even if the promise is already settled. They are scheduled on the microtask queue, which the engine processes only after the current synchronous code finishes.
This means synchronous code always runs first, and promise callbacks run before timers (setTimeout), which sit on the separate macrotask queue.
Even though the promise is resolved immediately and the timeout is 0, the promise callback (3) runs before the timeout callback (4), because the microtask queue is fully drained before the next macrotask runs.
Fetching Data from an API using promises
This example shows how to fetch data from a remote API using promises.
Chaining Promises
Promise chaining is a powerful feature that allows you to link multiple asynchronous operations together. Each .then returns a new promise, and whatever you return from a handler becomes the fulfillment value of that new promise — which is why the example below carries a value through several steps. For more information, check JavaScript: Promises and Chaining.
Integrating async/await with JavaScript Promises
Using async/await effectively can simplify the handling of asynchronous operations, making your code cleaner and easier to understand while maintaining all the power of JavaScript promises. You will learn more about it in JavaScript async/await, but here is a simple example.
Conclusion
Mastering JavaScript promises is crucial for any developer looking to manage asynchronous operations efficiently. Remember the core model: a promise is pending until the executor calls resolve or reject, after which it is settled forever; you read the result with .then/.catch/.finally; and those callbacks always run as microtasks, after the current synchronous code.
Where to go next
- JavaScript: Promises and Chaining — sequence asynchronous steps and pass values along.
- The Promise API — static helpers like
Promise.all,Promise.race,Promise.allSettled, andPromise.resolve. - Error Handling with Promises — rejections, thrown errors, and recovery patterns.
- JavaScript async/await — write promise-based code that reads like synchronous code.