JavaScript Promises Explained for Beginners

Why Promises Exist
The Problem with Callbacks
Before promises existed, developers used callbacks to handle asynchronous tasks. A callback is just a function passed into another function to be executed later. However, when you have multiple asynchronous tasks that depend on each other, callbacks create deeply nested, hard-to-read code.
Think of trying to read a book where every new paragraph is indented further and further to the right. By the time you reach the middle of the page, the text is squeezed against the edge, making it incredibly frustrating to read.
step1(() => {
step2(() => {
step3(() => {
console.log("Done");
});
});
});
Let us look at exactly what happens here
We start
step1. We cannot startstep2untilstep1is finished.Inside the callback for
step1, we startstep2.Inside the callback for
step2, we startstep3.This creates a messy triangle shape known as callback hell. It makes error handling difficult and destroys the readability of your code.
What is a Promise
What problem are we solving? We need a way to manage asynchronous code without nesting functions inside each other. A promise in JavaScript represents a value that is not available yet, but will be available in the future.
Think of going to a busy fast-food restaurant. You order your food and pay. The cashier does not hand you a burger immediately. Instead, they hand you a receipt with an order number. That receipt is a "promise". You can sit down and wait, knowing that eventually, the receipt will turn into your meal.
const promise = new Promise((resolve, reject) => {
resolve("Success");
});
Here is a step-by-step explanation:
We create a new promise using
new Promise().The promise takes a function with two parameters:
resolveandreject.If the background task finishes successfully, we call
resolve()and pass it the final data.If something goes wrong, we would call
reject()to report an error.
Promise States
A promise is not just a static object. It is a living process that moves through specific phases. A promise can only be in one of three states at any given time.
Think of an online shopping delivery. When you click buy, your package is "pending" delivery. If it arrives at your door, the delivery is "fulfilled". If the delivery truck breaks down and the package is lost, the delivery is "rejected".
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => resolve("Done"), 1000);
});
Let us explain these state transitions
Pending: The moment we create
myPromise, it enters the pending state. The timer is ticking, but no data is ready yet.Fulfilled: After exactly one second, the
setTimeoutfinishes and callsresolve("Done"). The promise permanently changes its state to fulfilled.Rejected: If we had a network error and called
reject("Error")instead, the promise would permanently change its state to rejected.
Working with Promises
Handling Success and Failure
Creating a promise is only half the battle. We also need to know how to extract the data when it succeeds, or catch the error when it fails. We do this using .then() and .catch().
Imagine your restaurant receipt again. When your number is called, you take your receipt to the counter to claim your food (using .then()). If the kitchen announces they ran out of ingredients, you take your receipt back to get a refund (using .catch()).
myPromise
.then(result => console.log(result))
.catch(error => console.log(error));
Here is the step-by-step execution
We attach
.then()directly to our promise. JavaScript waits quietly until the promise is fulfilled.Once fulfilled, the data inside
resolve()is passed directly into theresultvariable, and we print it.We also attach
.catch(). If the promise is rejected at any point, JavaScript skips.then()completely and runs the code inside.catch().
Promise Lifecycle
To fully grasp promises, you must understand their complete lifecycle flow. First, the promise is created and immediately enters the pending state. While it is pending, your main JavaScript code continues running. Eventually, the background task finishes, and the promise becomes either resolved (success) or rejected (failure). Finally, the result is handled by your attached .then() or .catch() methods.
Promise Chaining
One of the most powerful features of promises is chaining. We can connect multiple asynchronous steps cleanly, one after another, without nesting them.
Think of a car assembly line. Station 1 builds the frame and passes it to Station 2. Station 2 adds the engine and passes it to Station 3. Each station does its job and hands the result down the line.
getUser()
.then(user => getOrders(user))
.then(orders => console.log(orders))
.catch(err => console.log("An error occurred:", err));
Let us explain this clearly
We call
getUser(), which returns a promise.When the user is found, the first
.then()runs. We take that user data and pass it intogetOrders(), which returns another promise.When the orders are found, the second
.then()runs, and we print the orders.If an error occurs at any point in this entire chain, execution instantly jumps down to the single
.catch()at the bottom.
Why Promises are Better than Callbacks
Why were callbacks not enough? Promises vastly improve our code in three main ways
Avoid nesting: Promises flatten the code into a straight, vertical line.
Improve readability: Reading
.then()and.catch()flows like natural English.Better error handling: Instead of writing error checks for every single nested callback, a promise chain only needs one
.catch()at the very end to handle errors from any step.
Practical Understanding
Real-World Example
Let us look at a realistic example where we simulate fetching a user profile from a database.
function fetchUser() {
return new Promise(resolve => {
console.log("Fetching data...");
setTimeout(() => {
resolve({ name: "Purakhnath" });
}, 1000);
});
}
fetchUser()
.then(user => {
console.log("Welcome back, " + user.name);
})
.catch(err => {
console.log("Something went wrong");
});
Let us explain the flow of this real-world scenario
We define a function
fetchUserthat returns a new promise.Inside the promise, we start a background task using
setTimeoutto simulate network latency.We immediately call
fetchUser(). It prints "Fetching data..." and hands us back a pending promise.One second later, the timer finishes and calls
resolve()with our user object.Our attached
.then()catches that data and prints the welcome message.
Common Mistakes
When beginners start using promises, they often make these structural mistakes
Forgetting return in chaining: If you run another asynchronous function inside a
.then(), you must explicitlyreturnit. Otherwise, the next.then()will not wait for it to finish.Misunderstanding async timing: Developers sometimes try to access a promise's result outside of the
.then()block. The data only exists inside the.then().Not handling errors: If a promise is rejected and you forgot to add a
.catch(), JavaScript will throw an "unhandled promise rejection" error, which can crash your application.
Summary
Promises represent future values from asynchronous operations.
They have three distinct states: pending, fulfilled, and rejected.
They drastically improve readability by flattening nested code into clean vertical lines.
They allow multiple asynchronous tasks to be linked together via chaining.
They solve the core structural problems created by callback hell.
In the next article, we will explore async and await in JavaScript and see how they make asynchronous code even cleaner.




