Promises in JavaScript – Handling Asynchronous Operations
Over the past few days, I've been learning how JavaScript communicates with APIs using the Fetch API.
Today, I went deeper into one of the concepts that makes this possible—Promises.
Promises provide a way to handle operations whose results are available sometime in the future.
They are commonly used with API requests, timers, file operations, and many other asynchronous tasks.
Let's dive in.
What is a Promise?
A Promise is an object that represents the eventual completion or failure of an asynchronous operation.
For example:
const promise = new Promise((resolve, reject) => {
resolve("Operation completed!");
});
A Promise can eventually produce either:
A successful result
An error
Promise States
A Promise has three possible states:
1. Pending
The operation hasn't finished yet.
Pending
2. Fulfilled
The operation completed successfully.
Fulfilled
3. Rejected
The operation failed.
Rejected
The general flow is:
┌──→ Fulfilled
Pending ─────┤
└──→ Rejected
Once a Promise is fulfilled or rejected, it is settled.
Creating a Promise
We can create our own Promise using the Promise constructor.
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("Success!");
} else {
reject("Something went wrong!");
}
});
Here:
resolve()→ fulfills the Promisereject()→ rejects the Promise
Handling a Promise With then()
The .then() method runs when the Promise is fulfilled.
promise.then((result) => {
console.log(result);
});
Output:
Success!
Handling Errors With catch()
The .catch() method handles a rejected Promise.
promise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error);
});
This gives us a way to handle failures.
finally()
The .finally() method runs after the Promise settles, regardless of whether it was fulfilled or rejected.
promise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error);
})
.finally(() => {
console.log("Operation finished.");
});
This can be useful for cleanup or stopping a loading indicator.
Promise With setTimeout()
We can simulate an asynchronous operation using setTimeout().
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve("Data received!");
}, 2000);
});
promise.then((data) => {
console.log(data);
});
The Promise remains pending for approximately two seconds before becoming fulfilled.
Promise Chaining
Promises can be chained together.
Promise.resolve(10)
.then((number) => {
return number * 2;
})
.then((number) => {
return number + 5;
})
.then((result) => {
console.log(result);
});
Output:
25
Each .then() receives the value returned by the previous .then().
Why Promise Chaining is Useful
Promise chaining becomes useful when multiple asynchronous operations depend on one another.
For example:
Request User
↓
Get User ID
↓
Request User Posts
↓
Display Posts
Promises allow us to handle this sequence in an organized way.
Fetch Returns a Promise
One of the most important connections from the last few days is that:
fetch(url)
returns a Promise.
That's why we can write:
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error(error);
});
The Fetch API and Promises are closely connected.
Async Functions
An async function always returns a Promise.
async function greet() {
return "Hello!";
}
We can handle its result using .then():
greet().then((message) => {
console.log(message);
});
Output:
Hello!
The await Keyword
await can be used inside an async function to wait for a Promise to settle.
async function getData() {
const result = await Promise.resolve("Data received!");
console.log(result);
}
getData();
await makes asynchronous code easier to read because it allows us to write it in a more sequential style.
async/await With Fetch
Instead of:
fetch(url)
.then(response => response.json())
.then(data => console.log(data));
We can write:
async function getUsers() {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
const data = await response.json();
console.log(data);
}
getUsers();
For many developers, this style is easier to read.
Error Handling With try...catch
async/await works very well with try...catch.
async function getUsers() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
if (!response.ok) {
throw new Error("Request failed");
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
getUsers();
This gives us a clean structure for handling errors.
Promise.all()
Sometimes we need to run multiple independent asynchronous operations.
Promise.all() allows us to wait for multiple Promises.
const promise1 = Promise.resolve("User");
const promise2 = Promise.resolve("Posts");
Promise.all([promise1, promise2])
.then((results) => {
console.log(results);
});
Output:
["User", "Posts"]
The returned Promise fulfills when all of the supplied Promises fulfill.
Why Promise.all() is Useful
Suppose we need:
User Data
Posts
Comments
and none of these requests depends on the others.
We can run them together:
const [users, posts, comments] = await Promise.all([
fetch("/users"),
fetch("/posts"),
fetch("/comments")
]);
This can be more efficient than waiting for each independent request sequentially.
Best Practices
✔ Use async/await when it makes asynchronous code easier to read.
✔ Handle rejected Promises with .catch() or try...catch.
✔ Remember that fetch() returns a Promise.
✔ Check response.ok when using fetch().
✔ Use Promise.all() for independent asynchronous operations when appropriate.
✔ Avoid unnecessarily mixing multiple asynchronous patterns in the same piece of code.
My Biggest Takeaway
Today, I finally understood what is happening behind many of the asynchronous operations I've already been using.
A Promise represents a value that may become available in the future.
I learned how to create Promises, handle them using .then(), .catch(), and .finally(), and work with them more cleanly using async/await.
The connection with the Fetch API is now much clearer:
fetch() → Promise → Response → JSON → Data
