JavaScript Promises vs Async/Await — Complete Guide with Examples
If you have been writing JavaScript for a while, you have probably run into both Promises and async/await. And if you are like most developers, you probably wondered at some point — are these two the same thing? Do I need to learn both? Which one is better?
Let me clear this up once and for all in plain English.
First, Why Does This Even Exist?
Back in the day, JavaScript handled asynchronous tasks using callbacks. You know, the classic "function inside a function inside a function" situation. It looked something like this:
getUser(userId, function(user) {
getOrders(user.id, function(orders) {
getOrderDetails(orders[0].id, function(details) {
console.log(details);
});
});
});
This is what developers call callback hell. It is messy, hard to read, and an absolute nightmare to debug. So the JavaScript community came up with a better solution — Promises.
What Is a Promise?
A Promise is exactly what it sounds like. It is JavaScript's way of saying — "I don't have this value right now, but I promise I will get back to you."
A Promise has three possible states:
- Pending — still waiting for the result
- Fulfilled — got the result successfully
- Rejected — something went wrong
Here is a simple Promise in action:
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId) {
resolve({ id: userId, name: "Randhir", role: "Developer" });
} else {
reject(new Error("User ID is required"));
}
}, 1000);
});
}
fetchUserData(1)
.then(user => {
console.log("Got user:", user.name);
return user;
})
.then(user => {
console.log("User role:", user.role);
})
.catch(error => {
console.error("Something went wrong:", error.message);
})
.finally(() => {
console.log("Done — whether it worked or not");
});
This is much cleaner than callbacks. You chain .then() for
success, .catch() for errors, and .finally()
for anything that should run regardless of the result.
But here is the honest truth — once you start chaining multiple
.then() calls, it starts getting messy again. Not as bad
as callbacks, but still not the most natural thing to read.
Enter Async/Await
Async/await was introduced in ES2017 and it completely changed how most developers write asynchronous JavaScript. Here is the key thing to understand upfront:
Async/await is not a replacement for Promises. It is built on top of Promises. Under the hood, async/await is still using Promises. It just gives you a much cleaner way to write them.
Here is the same example from above, rewritten with async/await:
async function getUserData(userId) {
try {
const user = await fetchUserData(userId);
console.log("Got user:", user.name);
console.log("User role:", user.role);
} catch (error) {
console.error("Something went wrong:", error.message);
} finally {
console.log("Done — whether it worked or not");
}
}
getUserData(1);
See how much more readable that is? It reads almost like regular synchronous code, from top to bottom. No chaining, no nesting, just clean sequential logic.
The await keyword tells JavaScript — "pause here and
wait for this Promise to resolve before moving on."
Real World Example — Fetching Data From an API
Let me show you a real example fetching posts from an API, written both ways so you can see the actual difference side by side.
With Promises:
function getPosts() {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(posts => {
console.log('Total posts:', posts.length);
console.log('First post title:', posts[0].title);
})
.catch(error => {
console.error('Failed to fetch posts:', error.message);
});
}
getPosts();
With Async/Await:
async function getPosts() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const posts = await response.json();
console.log('Total posts:', posts.length);
console.log('First post title:', posts[0].title);
} catch (error) {
console.error('Failed to fetch posts:', error.message);
}
}
getPosts();
Same result, but the async/await version is far easier to follow — especially for developers who are newer to the codebase.
When Promises Are Actually Better
Here is something most tutorials skip — there are real situations where Promises are the better choice over async/await.
Running Multiple Tasks at the Same Time
If you have multiple independent async operations that do not depend on each other, you should run them in parallel instead of one after another.
Wrong way with async/await (slow):
async function loadDashboard() {
const user = await getUser(); // waits 1 second
const posts = await getPosts(); // then waits 1 more second
const weather = await getWeather(); // then waits 1 more second
// Total time: 3 seconds ❌
}
Right way with Promise.all (fast):
async function loadDashboard() {
const [user, posts, weather] = await Promise.all([
getUser(),
getPosts(),
getWeather()
]);
// Total time: ~1 second ✅ (all run at the same time)
}
Promise.all() runs everything simultaneously and waits for
all of them to finish. This can make a massive difference in real
applications — especially dashboards that load multiple data sources.
Other Useful Promise Methods
// Promise.race — returns whichever resolves first
const fastest = await Promise.race([apiCall1(), apiCall2()]);
// Promise.allSettled — waits for all, even if some fail
const results = await Promise.allSettled([apiCall1(), apiCall2()]);
// Promise.any — returns first successful one, ignores rejections
const first = await Promise.any([apiCall1(), apiCall2()]);
These methods have no direct async/await equivalent — you need Promises for these patterns. This is exactly why learning both is important.
Common Mistakes to Avoid
1. Forgetting the await Keyword
async function getUser() {
// ❌ Wrong — returns a Promise object, not actual data
const user = fetchUser();
console.log(user); // prints: Promise { <pending> }
// ✅ Correct
const user = await fetchUser();
console.log(user); // prints: { id: 1, name: "Randhir" }
}
2. Not Handling Errors With try/catch
// ❌ Wrong — unhandled rejection, can crash your app
async function getUser() {
const user = await fetchUser();
}
// ✅ Always wrap await calls in try/catch
async function getUser() {
try {
const user = await fetchUser();
console.log(user);
} catch (error) {
console.error("Failed:", error.message);
}
}
3. Using await Inside a Regular Function
// ❌ This throws a SyntaxError
function getUser() {
const user = await fetchUser(); // SyntaxError!
}
// ✅ The function MUST be declared async
async function getUser() {
const user = await fetchUser();
}
Promises vs Async/Await — Side by Side
| Promises | Async/Await | |
|---|---|---|
| Syntax | .then().catch() chaining |
Reads like sync code |
| Error Handling | .catch() |
try/catch |
| Parallel Execution | Promise.all() |
Promise.all() with await |
| Debugging | Harder to trace | Much easier |
| Readability | Gets messy when chained | Clean and linear |
| Browser Support | ES6+ | ES2017+ |
| Under the Hood | Promises | Still Promises |
So Which One Should You Actually Use?
Honest answer — use async/await by default for most situations. It is cleaner, easier to read, easier to debug, and your teammates will thank you for it.
But learn Promises too, because:
- Async/await is built on Promises — understanding them makes you a better developer
Promise.all(),Promise.race(), andPromise.allSettled()are essential for parallel operations- You will encounter Promise-based code in libraries and older codebases regularly
- Some advanced patterns simply cannot be done with async/await alone
The best developers use both — async/await for clean sequential logic, and Promise methods when running multiple tasks in parallel.
Final Thought
When I first learned this, I made the mistake of thinking I had to choose one and ignore the other. The truth is they work together. Once you are comfortable with both, you will naturally know which one fits each situation without even thinking about it.
Start by converting any .then() chains you already have
into async/await. You will immediately see how much cleaner your code
becomes — and you will never want to go back to callback hell again.
Got questions about a specific use case? Drop them in the comments or reach out via our contact page — happy to help.