JavaScript Promises Explained for Beginners
JS Promises for Beginners

What problem do promises solve?
JavaScript is single-threaded. It runs one piece of code at a time, which means when you perform a slow operation, like fetching data from a server, reading a file, or waiting for a timer.
The entire thread can't afford to stop and wait. Instead, JavaScript uses an event-driven, asynchronous model: kick off the work, continue with other things, and handle the result when it's ready.
The earliest pattern for this was the callback you pass a function to another function, and that function calls yours back when the work completes. This works, but it breaks down fast. Imagine fetching a user, then fetching their posts, then fetching comments on each post. You end up nesting callbacks inside callbacks inside callbacks code that fans rightward across the screen, where error handling is duplicated at every level and the logical sequence of operations is buried in indentation. Developers called this callback hell.
Promises were introduced to solve this. A promise represents a value that isn't available yet, but will be in the future or won't arrive at all because something went wrong. Instead of passing callbacks into every function, you work with an object that carries the result of an asynchronous operation, with a clean, chainable API for handling success and failure.
Promise states
Every promise lives in exactly one of three states at any given moment:
Pending: The operation is in progress. No result yet. This is the starting state.
Fulfilled: The operation completed successfully. The promise now holds the resolved value.
Rejected: The operation failed. The promise now holds a reason, typically an Error object explaining what went wrong.
The key rule: once a promise transitions out of pending, it never changes again. A fulfilled promise stays fulfilled forever. A rejected promise stays rejected. This predictability is one of the things that makes promises much easier to reason about than raw callbacks.
Creating a promise
You create a promise by calling the Promise constructor and passing it an executor, a function that receives two callbacks: resolve and reject. You call one of them when the async work finishes.
const fetchUser = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // pretend this comes from a network call
if (success) {
resolve({ id: 1, name: 'Priya' }); // fulfills the promise
} else {
reject(new Error('User not found')); // rejects the promise
}
}, 1000);
});
The executor runs immediately when new Promise(...) is called. The resolve and reject callbacks settle the promise and trigger any handlers that have been attached.
Handling success and failure
Once you have a promise, you react to its outcome using .then(), .catch(), and .finally():
fetchUser
.then((user) => {
console.log('Got user:', user.name); // runs if resolved
})
.catch((error) => {
console.error('Failed:', error.message); // runs if rejected
})
.finally(() => {
console.log('Done loading'); // always runs
});
.then() receives the resolved value. .catch() receives the rejection reason. .finally() receives nothing.
It's purely for cleanup (hiding a loading spinner, closing a connection). A critical point: .finally() doesn't consume the promise value. Whatever the promise settled with flows through .finally() unchanged to any subsequent handler.
Callbacks vs promises
Let's look at a concrete comparison. Three sequential async operations: load a user, then load their orders, then calculate their total spend.
loadUser(userId, function(err, user) {
if (err) return handleError(err);
loadOrders(user.id, function(err, orders) {
if (err) return handleError(err);
calculateSpend(orders, function(err, total) {
if (err) return handleError(err);
console.log('Total spend:', total);
});
});
});
With promises:
loadUser(userId)
.then((user) => loadOrders(user.id))
.then((orders) => calculateSpend(orders))
.then((total) => console.log('Total spend:', total))
.catch(handleError);
The logic is identical. But the promise version reads like a linear sequence of steps because it is one. Error handling is consolidated in a single .catch() at the end rather than duplicated at every level.
Promise chaining
Promise chaining is possible because .then() always returns a new promise. Whatever value you return from inside a .then() callback becomes the resolved value of that new promise, which the next .then() in the chain receives.
fetchUser(1)
.then((user) => {
return fetchPostsByUser(user.id); // returns another promise
})
.then((posts) => {
return posts.filter((p) => p.published); // returns a plain array
})
.then((publishedPosts) => {
console.log(publishedPosts.length, 'published posts');
})
.catch((err) => {
// any rejection in the chain above lands here
console.error('Something went wrong:', err.message);
});
The chain automatically unwraps promises: if you return a promise from inside .then(), the next .then() waits for that inner promise to settle before it fires. If you return a plain value, it's wrapped into a resolved promise automatically. This is the mechanism that makes chaining work seamlessly for both sync and async return values.
A note on async/await
Modern JavaScript introduced async/await as syntax sugar built directly on top of promises. An async function always returns a promise, and await pauses execution inside that function until a promise settles without blocking the thread. It makes promise-based code look synchronous:
async function loadDashboard(userId) {
try {
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);
const total = await calculateSpend(orders);
console.log('Total spend:', total);
} catch (err) {
console.error('Failed:', err.message);
}
}
This is the same sequence as the promise chain above, just written differently. Understanding promises deeply states, .then(), .catch(), chaining is essential before async/await makes sense, because under the hood, that's exactly what it's doing.





