Modern JavaScript applications frequently need to perform operations that don’t finish immediately. Fetching data from an API, reading files, waiting for a timer, or interacting with a database can all involve asynchronous operations.
JavaScript Promises provide a cleaner way to work with these asynchronous operations.
If you’ve ever used .then(), .catch(), or async/await, you’ve already encountered JavaScript Promises.
In this guide, we’ll explain what a JavaScript Promise is, how Promises work, the different Promise states, how to create and consume Promises, Promise chaining, error handling, Promise.all(), Promise.race(), and how Promises work with the JavaScript Event Loop.
What Is a JavaScript Promise?
A JavaScript Promise is an object that represents the eventual completion or failure of an asynchronous operation.
In simple terms, a Promise says:
“I don’t have the result right now, but I’ll provide it later.”
A Promise can represent three possible states:
- Pending — The operation is still in progress.
- Fulfilled — The operation completed successfully.
- Rejected — The operation failed.
The basic lifecycle looks like this:
Pending
|
+------> Fulfilled
|
+------> Rejected
Once a Promise becomes fulfilled or rejected, it is considered settled.
Why Do We Need Promises?
Before Promises became widely used, JavaScript developers commonly handled asynchronous operations using callbacks.
For example:
getUser(function(user) {
getOrders(user.id, function(orders) {
getPayment(orders, function(payment) {
console.log(payment);
});
});
});
As more asynchronous operations are added, the code can become deeply nested and difficult to maintain.
This problem is often called callback hell.
Promises provide a cleaner structure:
getUser()
.then(user => getOrders(user.id))
.then(orders => getPayment(orders))
.then(payment => {
console.log(payment);
})
.catch(error => {
console.error(error);
});
Promises make asynchronous code easier to organize and handle.
Promise States in JavaScript
Every Promise starts in the pending state.
For example:
const promise = new Promise((resolve, reject) => {
// Operation is pending
});
The Promise can then move to one of two final states.
1. Pending
The operation hasn’t finished yet.
Pending
2. Fulfilled
The operation completed successfully.
Pending → Fulfilled
3. Rejected
The operation failed.
Pending → Rejected
A Promise cannot go from fulfilled back to pending or from rejected back to fulfilled.
Once it is settled, its state cannot change.
How to Create a Promise in JavaScript
You can create a Promise using the Promise constructor:
const myPromise = new Promise((resolve, reject) => {
// asynchronous operation
});
The constructor receives a function with two arguments:
resolve
reject
resolve()
Call resolve() when the operation succeeds.
const myPromise = new Promise((resolve, reject) => {
resolve("Operation successful");
});
reject()
Call reject() when the operation fails.
const myPromise = new Promise((resolve, reject) => {
reject("Something went wrong");
});
A complete example:
const myPromise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("Task completed successfully");
} else {
reject("Task failed");
}
});
Consuming a Promise With .then()
The .then() method is used to handle a successful Promise.
myPromise.then((result) => {
console.log(result);
});
If the Promise is fulfilled, the value passed to resolve() becomes available inside .then().
For example:
const promise = new Promise((resolve) => {
resolve("Hello from Promise");
});
promise.then((result) => {
console.log(result);
});
Output:
Hello from Promise
Handling Promise Errors With .catch()
The .catch() method handles rejected Promises.
const promise = new Promise((resolve, reject) => {
reject("Something went wrong");
});
promise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error);
});
Output:
Something went wrong
Using .catch() is important because asynchronous operations can fail for many reasons.
For example:
- Network failure
- Invalid input
- Server error
- Database failure
- Authentication failure
Using .finally()
The .finally() method runs regardless of whether the Promise is fulfilled or rejected.
promise
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error);
})
.finally(() => {
console.log("Operation finished");
});
This is useful for cleanup operations.
For example, you might hide a loading indicator after an API request completes:
fetchData()
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
})
.finally(() => {
hideLoadingSpinner();
});
Promise Chaining
One of the most useful features of JavaScript Promises is Promise chaining.
You can execute multiple asynchronous operations in sequence.
getUser()
.then(user => {
return getOrders(user.id);
})
.then(orders => {
return getPayment(orders);
})
.then(payment => {
console.log(payment);
})
.catch(error => {
console.error(error);
});
Each .then() can return another Promise.
The next .then() waits for that Promise to settle.
This makes complex asynchronous workflows easier to manage.
Returning Values From .then()
A .then() callback can return a normal value.
Promise.resolve(10)
.then(value => {
return value * 2;
})
.then(value => {
console.log(value);
});
Output:
20
The returned value becomes the input for the next .then().
You can also return another Promise:
Promise.resolve(10)
.then(value => {
return Promise.resolve(value * 2);
})
.then(value => {
console.log(value);
});
This allows you to build sequences of asynchronous operations.
Promise and setTimeout()
Let’s look at how Promises interact with timers.
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
Output:
Start
End
Promise
Timeout
Why does the Promise run before setTimeout()?
Promise callbacks are scheduled as microtasks, while timer callbacks are scheduled as tasks.
After synchronous code finishes, JavaScript processes the microtask queue before moving to the next task.
This behavior is closely related to the JavaScript Event Loop.
Promises and the JavaScript Event Loop
Promises do not execute their .then() callbacks immediately.
Consider:
console.log("A");
Promise.resolve().then(() => {
console.log("B");
});
console.log("C");
The output is:
A
C
B
The synchronous statements execute first.
The Promise callback is then placed in the microtask queue.
After the current call stack becomes empty, the Event Loop processes the microtask.
Therefore:
A
C
B
Understanding this behavior is important when debugging asynchronous JavaScript.
Promise.resolve()
Promise.resolve() creates a fulfilled Promise.
const promise = Promise.resolve("Hello");
promise.then(value => {
console.log(value);
});
Output:
Hello
It can also be useful when you need to convert a value into a Promise.
Promise.resolve(100)
.then(value => {
console.log(value);
});
Promise.reject()
Promise.reject() creates a rejected Promise.
const promise = Promise.reject("Something went wrong");
promise.catch(error => {
console.error(error);
});
This is useful when you need to create a rejected Promise directly.
Promise.all()
Promise.all() is useful when you need to execute multiple independent asynchronous operations and wait for all of them to complete.
For example:
const users = fetch("/users");
const products = fetch("/products");
const orders = fetch("/orders");
Promise.all([users, products, orders])
.then(results => {
console.log(results);
})
.catch(error => {
console.error(error);
});
Promise.all() fulfills when all supplied Promises fulfill.
However, if one Promise rejects, the combined Promise rejects.
A simple example:
const promise1 = Promise.resolve("One");
const promise2 = Promise.resolve("Two");
const promise3 = Promise.resolve("Three");
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log(results);
});
The result is an array containing the results.
["One", "Two", "Three"]
Promise.allSettled()
Sometimes you want to know the result of every Promise, even when some operations fail.
That’s where Promise.allSettled() is useful.
const promises = [
Promise.resolve("Success"),
Promise.reject("Failed"),
Promise.resolve("Another success")
];
Promise.allSettled(promises)
.then(results => {
console.log(results);
});
Instead of stopping at the first rejection, Promise.allSettled() waits for all Promises to settle.
This can be useful when multiple operations should be evaluated independently.
Promise.race()
Promise.race() returns the result of whichever Promise settles first.
const promise1 = new Promise(resolve => {
setTimeout(() => resolve("First"), 1000);
});
const promise2 = new Promise(resolve => {
setTimeout(() => resolve("Second"), 2000);
});
Promise.race([promise1, promise2])
.then(result => {
console.log(result);
});
Output:
First
because the first Promise settles earlier.
Promise.any()
Promise.any() resolves when the first Promise fulfills.
const promises = [
Promise.reject("Error 1"),
Promise.resolve("Success"),
Promise.resolve("Another success")
];
Promise.any(promises)
.then(result => {
console.log(result);
});
Output:
Success
If all Promises reject, Promise.any() rejects with an AggregateError.
Promise.all vs Promise.allSettled vs Promise.race vs Promise.any
These methods solve different problems.
| Method | Behavior |
|---|---|
Promise.all() | Waits for all; rejects if one rejects |
Promise.allSettled() | Waits for all regardless of success or failure |
Promise.race() | Settles when the first Promise settles |
Promise.any() | Fulfills when the first Promise fulfills |
Choosing the correct method depends on your application’s requirements.
Promises With Fetch API
One of the most common uses of Promises is making HTTP requests with the Fetch API.
For example:
fetch("https://api.example.com/users")
.then(response => {
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error("Request failed:", error);
});
The fetch() function returns a Promise.
Once the HTTP response is available, the Promise fulfills and the first .then() executes.
The call to:
response.json()
also returns a Promise.
That’s why another .then() is used to access the parsed JSON data.
Promises With async/await
Modern JavaScript developers often use async/await instead of long Promise chains.
For example:
async function getUsers() {
try {
const response = await fetch(
"https://api.example.com/users"
);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
Although this code looks synchronous, it is still based on Promises.
The await keyword pauses the execution of the current async function until the Promise settles, without blocking the JavaScript thread.
Promise vs Callback
Callbacks and Promises can both handle asynchronous operations.
Callback
getUser((user) => {
getOrders(user.id, (orders) => {
console.log(orders);
});
});
Promise
getUser()
.then(user => getOrders(user.id))
.then(orders => {
console.log(orders);
});
Promises generally make asynchronous workflows easier to compose and handle, especially when multiple operations depend on one another.
Common Promise Mistakes
1. Forgetting to Return a Promise
Consider:
getUser()
.then(user => {
getOrders(user.id);
})
.then(orders => {
console.log(orders);
});
The first .then() doesn’t return the getOrders() Promise.
A better version is:
getUser()
.then(user => {
return getOrders(user.id);
})
.then(orders => {
console.log(orders);
});
Or using an implicit return:
getUser()
.then(user => getOrders(user.id))
.then(orders => {
console.log(orders);
});
2. Ignoring Errors
Avoid creating asynchronous operations without handling possible failures.
Instead of:
fetchData();
consider:
fetchData()
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
3. Creating Unnecessary Promises
You don’t always need to manually create a Promise.
For example, this is unnecessary:
function getData() {
return new Promise(resolve => {
resolve("Data");
});
}
If you already have a value, you may simply return it.
Manual Promise creation is most useful when you’re wrapping an operation that doesn’t already provide a Promise-based interface.
4. Running Independent Operations Sequentially
Suppose you have two independent API requests.
This:
const users = await getUsers();
const products = await getProducts();
waits for the first operation before starting the second.
If the operations are independent, you may be able to run them concurrently:
const [users, products] = await Promise.all([
getUsers(),
getProducts()
]);
This can reduce total waiting time.
Are Promises Synchronous or Asynchronous?
Promises themselves represent asynchronous results, but creating a Promise does not mean every part of its executor is asynchronous.
For example:
console.log("Before");
const promise = new Promise(resolve => {
console.log("Inside Promise");
resolve();
});
console.log("After");
The output is:
Before
Inside Promise
After
The Promise executor runs immediately.
However, callbacks attached through .then(), .catch(), and .finally() are executed asynchronously through the microtask mechanism.
This distinction is important when understanding JavaScript Promises.
Best Practices for Using JavaScript Promises
Handle Errors
Always consider what happens if an asynchronous operation fails.
Use Promise.all for Independent Operations
When multiple independent operations can run concurrently, Promise.all() can be useful.
Avoid Deep Promise Chains
If a workflow becomes difficult to read, consider using async/await.
Return Promises Correctly
When chaining Promises, return the Promise from each .then() when the next step depends on it.
Avoid Unnecessary Promise Wrapping
Don’t create a new Promise when an existing API already returns one.
Keep Asynchronous Functions Focused
Breaking complex workflows into smaller functions makes Promise-based code easier to understand and test.
JavaScript Promises: A Simple Mental Model
If you’re new to Promises, remember this:
Start asynchronous operation
↓
Pending
/ \
↓ ↓
Success Failure
↓ ↓
resolve() reject()
↓ ↓
.then() .catch()
And for multiple operations:
Promise.all()
↓
Wait for all
↓
All successful?
/ \
Yes No
↓ ↓
Result Reject
This mental model is enough to understand most common Promise scenarios.
Why Are JavaScript Promises Important?
JavaScript Promises are a fundamental part of modern asynchronous programming.
They are used extensively in:
- Fetch API
- Node.js
- React applications
- Next.js applications
- Database operations
- REST APIs
- File operations
- Timers
- Authentication
- Third-party APIs
- AI APIs
Understanding Promises also makes it much easier to understand async/await and the JavaScript Event Loop.
Conclusion
JavaScript Promises provide a structured way to handle asynchronous operations.
A Promise represents a value that may become available in the future and can be either pending, fulfilled, or rejected.
The most important Promise concepts to remember are:
resolve()represents successful completion.reject()represents failure..then()handles successful results..catch()handles errors..finally()runs after the Promise settles.Promise.all()waits for multiple operations.Promise.allSettled()waits for all results regardless of failure.Promise.race()returns the first settled result.Promise.any()returns the first fulfilled result.async/awaitprovides a cleaner syntax for working with Promises.- Promise callbacks are processed through the microtask queue.
Once you understand JavaScript Promises, asynchronous programming becomes much easier to reason about. The next step is to learn async/await, the JavaScript Event Loop, and how asynchronous operations interact with APIs and the browser runtime.
Frequently Asked Questions
What is a Promise in JavaScript?
A Promise is an object representing the eventual success or failure of an asynchronous operation.
What are the three states of a Promise?
A Promise can be pending, fulfilled, or rejected.
What is the difference between resolve and reject?
resolve() indicates that an operation completed successfully, while reject() indicates that the operation failed.
What is the difference between .then() and .catch()?
.then() is used to handle a fulfilled Promise, while .catch() is used to handle a rejected Promise.
Are JavaScript Promises asynchronous?
Promise callbacks such as .then() and .catch() execute asynchronously as microtasks, although the executor function passed to new Promise() runs immediately.
Why use Promise.all()?
Promise.all() is useful when you need to wait for multiple independent Promises to complete successfully.
Are async/await and Promises the same?
async/await is syntax built around Promises. An async function always returns a Promise, and await is used to wait for a Promise’s result inside an async function.
Are Promises better than callbacks?
Promises generally provide a cleaner way to compose asynchronous operations and handle errors, particularly when multiple asynchronous operations need to be chained together.




