Promises
This lesson explains Promises with clear examples, use cases, and best practices so you can use modern JavaScript confidently in real projects.
ES6+ Promises Overview
A Promise is an object that represents the eventual
result of an asynchronous operation. It helps you manage async code
without deeply nested callbacks and gives you a clear way to handle
success and failure.
Promises are the foundation of modern async JavaScript. They power
the Fetch API, database calls, file uploads, timers, and
async/await syntax.
| Feature | Description |
| Introduced In | ES6 (ECMAScript 2015) |
| States | Pending, fulfilled, rejected |
| Main Methods | .then(), .catch(), .finally() |
| Creation | new Promise((resolve, reject) => ...) |
| Static Helpers | Promise.resolve(), Promise.reject() |
| Common Uses | API requests, timers, file I/O, async workflows |
Basic Promise Example
const promise =
new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("Data loaded");
} else {
reject(
new Error("Failed to load")
);
}
});
promise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error.message);
});
A Promise starts in the pending state. It becomes
fulfilled when resolve() runs or
rejected when reject() runs.
How Promises Work
Promises let you attach handlers for success and failure instead of
passing callbacks deeper and deeper into nested functions.
| Step | Description | Example |
| Create Promise | Start async work inside the executor. | new Promise(...) |
| Pending State | Operation is still running. | Waiting for API response |
| Resolve | Operation succeeds with a value. | resolve(data) |
| Reject | Operation fails with an error. | reject(error) |
| Handle Result | Use then, catch, or finally. | .then(...) |
Promise States
| State | Meaning | What Happens Next |
| Pending | Async work is still in progress. | Can move to fulfilled or rejected. |
| Fulfilled | Operation completed successfully. | .then() handlers run. |
| Rejected | Operation failed. | .catch() handlers run. |
then, catch, and finally
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({
id,
name: "Alex"
});
} else {
reject(
new Error("Invalid user id")
);
}
}, 1000);
});
}
fetchUser(1)
.then((user) => {
console.log(user.name);
return user.id;
})
.then((id) => {
console.log("User ID:", id);
})
.catch((error) => {
console.error(error.message);
})
.finally(() => {
console.log("Request finished");
});
.then() handles success,
.catch() handles errors, and
.finally() runs whether the Promise succeeds or fails.
Promise Chaining
function getToken() {
return Promise.resolve("abc123");
}
function getProfile(token) {
return Promise.resolve({
token,
name: "Sam"
});
}
function getSettings(profile) {
return Promise.resolve({
...profile,
theme: "dark"
});
}
getToken()
.then(getProfile)
.then(getSettings)
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error(error);
});
Each .then() can return a value or another Promise.
This creates a readable async pipeline instead of callback nesting.
Fetch API with Promises
fetch(
"https://jsonplaceholder.typicode.com/users/1"
)
.then((response) => {
if (!response.ok) {
throw new Error(
"Network response failed"
);
}
return response.json();
})
.then((user) => {
console.log(user.name);
console.log(user.email);
})
.catch((error) => {
console.error(error.message);
});
The Fetch API returns a Promise, making it one of the most common
real-world uses of Promises in frontend development.
Promise.resolve and Promise.reject
const success =
Promise.resolve("Ready");
const failure =
Promise.reject(
new Error("Something went wrong")
);
success.then((value) => {
console.log(value);
});
failure.catch((error) => {
console.error(error.message);
});
These static methods quickly create already-settled Promises. They
are useful for wrapping values or starting error flows in tests and
utilities.
Promise Error Handling
function divide(a, b) {
return new Promise((resolve, reject) => {
if (b === 0) {
reject(
new Error("Cannot divide by zero")
);
return;
}
resolve(a / b);
});
}
divide(10, 0)
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error.message);
});
divide(10, 2)
.then((result) => {
console.log(result);
});
Always handle rejected Promises with .catch() or by
returning the Promise to a caller that handles errors. Unhandled
rejections can cause hard-to-debug runtime issues.
Promise with setTimeout
function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
wait(1500)
.then(() => {
console.log("Done waiting");
});
Wrapping timers in Promises is a common pattern for delays,
retries, polling, and staged async workflows.
Callbacks vs Promises
// Callback style
function loadUserCallback(id, callback) {
setTimeout(() => {
callback(null, { id, name: "Alex" });
}, 500);
}
// Promise style
function loadUserPromise(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: "Alex" });
}, 500);
});
}
loadUserPromise(1)
.then((user) => {
console.log(user);
});
Promises reduce callback nesting and make async flows easier to
compose, chain, and debug compared to traditional callback patterns.
Promises vs async/await
| Feature | Promises | async/await |
| Syntax | .then() and .catch() | await inside async functions |
| Readability | Good for pipelines and chaining. | Looks closer to synchronous code. |
| Foundation | Core async primitive. | Built on top of Promises. |
| Error Handling | .catch() | try/catch |
Common Promise Use Cases
- Fetching data from APIs with Fetch or Axios.
- Reading files or database records asynchronously.
- Running delayed or scheduled tasks with timers.
- Chaining multiple async steps in order.
- Wrapping callback-based APIs in Promise-based helpers.
- Handling success and failure in UI loading states.
- Building the base layer for
async/await functions.
Promise Best Practices
- Always handle errors with
.catch() or upstream handlers. - Return Promises from
.then() when chaining async work. - Prefer
async/await for long sequential flows when readability matters. - Reject with meaningful
Error objects, not raw strings. - Use
.finally() for cleanup such as hiding loaders. - Avoid creating unnecessary Promise wrappers around sync values.
- Keep Promise chains flat and focused on one workflow.
Common Promise Mistakes
- Forgetting to return a Promise inside
.then(). - Not handling rejected Promises at all.
- Creating callback hell inside Promise executors.
- Calling
resolve() and reject() more than once. - Mixing callbacks and Promises in the same function without a clear pattern.
- Assuming
.then() runs synchronously. - Throwing errors outside a
.catch() or rejected flow.
Key Takeaways
- Promises represent the future result of async work.
- They have three states: pending, fulfilled, and rejected.
- Use
.then(), .catch(), and .finally() to handle outcomes. - Promise chaining makes async workflows easier to read and maintain.
- Promises are the foundation for Fetch, timers, and async/await.
Pro Tip
When chaining Promises, always
return the next Promise from
.then(). Without a return, the next step receives
undefined instead of the async result.