Course: JavaScript Intermediate
Lesson 15: Promises
Level
Intermediate
Estimated Reading Time
10–15 minutes
Goal
By the end of this lesson, the learner will be able to:
- Create a Promise using
new Promise(resolve, reject) - Handle resolved and rejected Promises with
.then(),.catch(), and.finally() - Explain what a Promise state is and how it transitions from pending to settled
Frontend Development Context
Promises are the foundation of asynchronous JavaScript. The Fetch API returns a Promise. setTimeout can be wrapped in a Promise. Any time you need to represent a value that is not available yet — a network response, a file read, a timer — a Promise is the tool for it. Understanding how to create and consume Promises is a prerequisite for understanding async/await, which builds directly on them.
Creating your own Promises is also a skill you will use when wrapping older callback-based APIs into something more modern, or when you need to build utilities that perform work asynchronously and return their results in a predictable way.
Explanation
A Promise is an object representing the eventual completion or failure of an asynchronous operation. It has three states: pending (the operation has not finished yet), fulfilled (the operation succeeded), or rejected (the operation failed). Once a Promise is settled (fulfilled or rejected), its state cannot change.
You create a Promise with new Promise((resolve, reject) => {...}). The constructor takes a function called the executor, which runs immediately. Inside the executor, you call resolve(value) when the operation succeeds and reject(reason) when it fails. Calling one of these is what transitions the Promise from pending to settled.
.then(onFulfilled) attaches a handler that runs when the Promise resolves. It receives the resolved value as its argument. .then() itself returns a new Promise, which is how chaining works.
.catch(onRejected) attaches a handler that runs when the Promise rejects, or when any previous .then() throws an error. It is equivalent to .then(null, onRejected) but more readable.
.finally(onFinally) runs after the Promise settles regardless of whether it was fulfilled or rejected. It does not receive the value or error — it is purely for cleanup code like hiding loading indicators or closing connections.
Code Example
This example creates a Promise manually, then consumes it with .then(), .catch(), and .finally().
// Create a Promise that simulates fetching a lesson by ID
function fetchLesson(id) {
return new Promise(function (resolve, reject) {
// Simulate async work (e.g., a network call)
setTimeout(function () {
if (id > 0) {
resolve({ id: id, title: `Lesson ${id}`, level: "Intermediate" });
} else {
reject(new Error("Invalid lesson ID: must be greater than 0"));
}
}, 300);
});
}
// Consume the Promise
fetchLesson(5)
.then(function (lesson) {
console.log("Loaded:", lesson.title); // "Loaded: Lesson 5"
return lesson.title.toUpperCase(); // return a new value to the next .then()
})
.then(function (title) {
console.log("Uppercase title:", title); // "Uppercase title: LESSON 5"
})
.catch(function (error) {
console.error("Error:", error.message);
})
.finally(function () {
console.log("Done — hide the loading spinner");
});
// Test the rejection path
fetchLesson(-1)
.then(lesson => console.log(lesson))
.catch(err => console.error("Caught:", err.message)); // "Caught: Invalid lesson ID..."
Code Explanation
fetchLesson returns a new Promise. The executor function runs immediately and starts a setTimeout to simulate an asynchronous delay. After 300ms, if the id is valid, resolve is called with a lesson object. If not, reject is called with an Error.
The first .then(function(lesson) {...}) receives the resolved lesson object. It logs the title, then returns lesson.title.toUpperCase(). Returning a value from .then() wraps it in a resolved Promise, passing it to the next .then() in the chain.
The second .then(function(title) {...}) receives the uppercase string. This demonstrates how .then() chains pass values forward. Each .then() can transform the result and pass a new value to the next step.
.catch() handles any rejection in the chain. When fetchLesson(-1) is called, reject is triggered with an Error, and .catch() receives that error. .finally() on the first chain runs regardless — useful for hiding loading states that should always clear.
Pattern Highlights
✅ Positive Pattern: Return values from
.then()callbacks to pass them forward through the chain. This keeps the chain flat and readable, rather than nesting callbacks inside each other.
⚠️ Neutral Note: A Promise can only be resolved or rejected once. Calling both
resolveandrejectin the same executor, or calling either one more than once, has no effect after the first call.
❌ Negative Pattern: Do not create a Promise to wrap something that is already synchronous.
new Promise((resolve) => resolve(value))for a non-async value adds unnecessary complexity. Just return the value directly.
Common Mistakes
Mistake 1: Forgetting to return the Promise from a function
Not returning the Promise means the caller cannot chain .then() on it.
function loadData() {
new Promise(resolve => setTimeout(() => resolve("data"), 100));
// missing return!
}
loadData().then(data => console.log(data)); // TypeError: Cannot read properties of undefined
Always return new Promise(...) from the function so the caller can attach handlers.
Mistake 2: Catching an error and then re-throwing without a message
Swallowing or silently catching errors makes debugging impossible.
fetchLesson(0)
.catch(function (err) {
// no logging, no re-throw, error is silently ignored
});
At minimum, log the error in .catch(). If the calling code needs to know about the failure, re-throw the error or return a rejected Promise from the catch handler.
Mistake 3: Nesting .then() instead of chaining
Creating callback-style nesting inside .then() handlers.
fetchLesson(1).then(function (lesson) {
fetchLesson(2).then(function (lesson2) { // nested — hard to read and error-prone
console.log(lesson, lesson2);
});
});
Return the inner Promise and chain it flat: fetchLesson(1).then(() => fetchLesson(2)).then(lesson2 => ...). For multiple parallel requests, use Promise.all.
Quick Recap
- A Promise is created with
new Promise((resolve, reject) => {...})and represents a value available in the future - Calling
resolve(value)fulfills the Promise; callingreject(error)rejects it — the state cannot change after that .then()handles fulfillment;.catch()handles rejection;.finally()runs in both cases.then()returns a new Promise — you can chain multiple.then()calls to transform values step by step