Understanding JavaScript Promises: A Beginner-Friendly Guide
A visual overview of asynchronous JavaScript execution
If you've spent any time writing JavaScript, you've probably run into a Promise sooner or later. They can look intimidating at first, but once the core idea clicks, they make asynchronous code far easier to read and reason about.
In this guide, we'll break down what a Promise actually is, walk through the states it can be in, and build a couple of small examples together — including how async/await fits into the picture.
What is a Promise?
A Promise is an object that represents the eventual result of an asynchronous operation. Instead of writing a callback and hoping it fires at the right time, a Promise gives you a predictable object you can attach handlers to.
Think of a Promise as a receipt for work that hasn't finished yet — you can hold onto it and decide what to do once the work completes.
The three Promise states
Every Promise exists in one of three states:
- Pending — the initial state, neither fulfilled nor rejected
- Fulfilled — the operation completed successfully
- Rejected — the operation failed
Once a Promise settles (fulfilled or rejected), it cannot change state again. This immutability is what makes Promises predictable to work with.
Creating your own Promise
Here's a simple example that wraps a timeout in a Promise:
function wait(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Done waiting!");
}, ms);
});
}
wait(1000).then((message) => {
console.log(message);
});
The executor function runs immediately, and calling resolve() or reject() moves the Promise out of its pending state.
Chaining multiple steps
One of the biggest wins with Promises is chaining. Each .then() returns a new Promise, so you can build a readable sequence of steps instead of nesting callbacks inside callbacks.
Using async/await
async/await is syntax built on top of Promises that lets asynchronous code read like synchronous code:
async function loadData() {
try {
const result = await wait(1000);
console.log(result);
} catch (err) {
console.error("Something went wrong:", err);
}
}
loadData();
Under the hood, this is still Promise-based — await simply pauses execution of the function until the Promise settles.
Common mistakes to avoid
- Forgetting to return a Promise inside a
.then()chain, breaking the sequence - Mixing
awaitwith.then()unnecessarily, making code harder to follow - Not handling rejections, which can cause silent failures
- Creating a Promise around code that's already synchronous
Promises take a little practice, but once you're comfortable with them, asynchronous JavaScript stops feeling unpredictable. Start small — wrap one callback-based function you use often, and build from there.