Promises vs Async/Await Explained: A Friendly Guide for JavaScript Beginners.So, you’ve started learning JavaScript, and everything is going great. You know how to create variables, loop through arrays, and write functions. Then, you hit a wall. You try to get data from a website or read a file, and suddenly, things aren’t happening in the order you expect. Your console.log() statements are printing in the wrong order, and you’re getting errors like undefined or Promise {<pending>}.
Promises vs Async/Await Explained

Welcome to the world of asynchronous programming! Don’t worry; it trips everyone up at first.
What’s the Problem? The Need for Asynchronous Code
Before we talk about the solutions, let’s look at the problem. JavaScript is typically “single-threaded,” which is a fancy way of saying it can only do one thing at a time. But thanks to the magic of the browser or Node.js, it can offload slow tasks (like fetching data from a server) and keep running the rest of your code.
Imagine you have a button on your page. When you click it, you want to get some user info from a database. If that database request was synchronous, your entire webpage would freeze—buttons wouldn’t click, menus wouldn’t open—until the data came back. This would be a terrible user experience.
Part 1: Understanding Promises (The “Receipt”)
A Promise can be in one of three states at any time:
- Pending: The initial state. The operation is still happening, and we don’t have a result yet. (Like waiting for your coffee).
- Fulfilled (Resolved): The operation completed successfully, and we now have the result. (You got your coffee!).
- Rejected: The operation failed, and we have an error. (The café ran out of coffee beans).
Creating Your First Promise
You don’t always create Promises, but it’s important to see what’s happening under the hood. Many modern APIs (like fetch) return Promises for you.
Here’s how you might create a simple Promise to simulate a coffee order:
const coffeeOrder = new Promise((resolve, reject) => {
// This is the "asynchronous work"
setTimeout(() => {
const coffeeIsReady = true; // Simulate success or failure
if (coffeeIsReady) {
resolve("Your latte is ready!"); // This moves the promise to "fulfilled"
} else {
reject("Sorry, we're out of milk."); // This moves the promise to "rejected"
}
}, 2000); // Simulate a 2-second wait
});
console.log(coffeeOrder); // You'll see: Promise {<pending>}If you run this, you’ll see Promise {<pending>} logged to the console. The promise is still working! But how do we actually get the result (the “Your latte is ready!” message) when it’s done? That’s where .then() and .catch() come in.
Consuming a Promise: .then() and .catch()
To get the value from a promise, we attach a callback function using .then() for success and .catch() for errors.
const coffeeOrder = new Promise((resolve, reject) => {
setTimeout(() => {
const coffeeIsReady = true;
if (coffeeIsReady) {
resolve("Your latte is ready!");
} else {
reject("Sorry, we're out of milk.");
}
}, 2000);
});
// This is how we "consume" the promise
coffeeOrder
.then((message) => {
// This function runs when the promise is "fulfilled"
console.log("Success:", message); // Output after 2 secs: Success: Your latte is ready!
})
.catch((error) => {
// This function runs when the promise is "rejected"
console.error("Error:", error);
});
console.log("Order placed! Waiting for it to be ready...");
// This logs IMMEDIATELY, while the promise is still pending.Chaining Promises: The Good and The Bad

Imagine a simple app that fetches a user, then fetches their posts:
function fetchUser(userId) {
return new Promise((resolve) => {
setTimeout(() => resolve({ id: userId, name: "Alice" }), 1000);
});
}
function fetchPosts(user) {
return new Promise((resolve) => {
setTimeout(() => resolve([`${user.name}'s post 1`, `${user.name}'s post 2`]), 1000);
});
}
// Promise Chaining
fetchUser(123)
.then((user) => {
console.log("User fetched:", user);
// We return the next promise here
return fetchPosts(user);
})
.then((posts) => {
// This .then() waits for fetchPosts to finish
console.log("Posts fetched:", posts);
})
.catch((error) => {
// A single .catch() at the end handles errors from ANY of the steps above
console.error("Something went wrong:", error);
});This is way cleaner than nested callbacks! You can see a clear, linear flow. However, for very complex chains with a lot of logic inside the .then() blocks, it can still get a little messy and hard to read. That’s where its modern cousin comes in.
Part 2: Async/Await (The Modern Way)
The Magic Words: async and await
We use two keywords:
async: You put this before a function declaration. It does two things: 1) It makes the function always return a Promise. 2) It allows you to use theawaitkeyword inside it.await: You put this in front of a Promise. It tells JavaScript: “Pause the execution of thisasyncfunction until this Promise settles, and then give me its result.” It’s like putting a bookmark in your book and waiting for that specific page to load.
Refactoring Our Coffee Order
Let’s rewrite our coffee order with async/await:
function placeOrder() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const coffeeIsReady = true;
if (coffeeIsReady) {
resolve("Your latte is ready!");
} else {
reject("Sorry, we're out of milk.");
}
}, 2000);
});
}
// This is the async function
async function getCoffee() {
console.log("Order placed! Waiting...");
try {
// We await the promise! This line pauses the function, but not the whole program.
const message = await placeOrder();
// This runs after the 2 seconds are up
console.log("Success:", message);
} catch (error) {
// If the promise is rejected, the error lands here
console.error("Error:", error);
}
}
getCoffee();
console.log("Doing other things while waiting..."); // This runs immediately!Handling Errors with Try…Catch

It makes debugging so much easier. You can put breakpoints inside the try block and step through your asynchronous code line by line, just like it was synchronous.
Refactoring Our User/Posts Example
Now let’s look at our user and posts example from earlier. See how much cleaner it looks:
// The same promise-returning functions from before
function fetchUser(userId) { /* ... */ }
function fetchPosts(user) { /* ... */ }
async function displayUserPosts(userId) {
try {
const user = await fetchUser(userId);
console.log("User fetched:", user);
const posts = await fetchPosts(user);
console.log("Posts fetched:", posts);
} catch (error) {
console.error("Something went wrong:", error);
}
}
displayUserPosts(123);The Big Face-Off: Promises vs. Async/Await
So, when do you use which? It’s not an either/or question; it’s about choosing the right tool for the job. Here’s a handy comparison:
| Feature | Promises (.then()) | Async/Await |
|---|---|---|
| Readability | Good for simple chains, but can get messy with complex flows. | Excellent. Code looks like standard synchronous code. |
| Error Handling | Uses .catch() method. Good for a single catch-all at the end of a chain. | Uses try...catch blocks. Very intuitive and familiar. |
| Debugging | Can be tricky. Stepping through .then() blocks in a debugger isn’t always smooth. | A dream to debug. You can set breakpoints and step over await lines like any other line of code. |
| Under the Hood | The native way to handle async operations. | Syntactic sugar built on top of Promises. You must understand Promises to use async/await effectively. |
| Complex Logic | Powerful for functional programming patterns. | Perfect for linear, step-by-step logic where each step depends on the last. |
The Ultimate Pro Tip: Use Them Together!
The strength of Promises isn’t just chaining; it’s in their helper methods like Promise.all() and Promise.race(). These methods are fantastic for performance.
The Danger of Sequential Awaits
Look at this code. What’s wrong with it?
async function loadDashboard() {
const user = await fetchUser(); // This takes 2 seconds
const posts = await fetchPosts(); // This waits for user, then takes 2 more seconds
const comments = await fetchComments(); // This waits for posts, then takes 2 seconds
// Total time: ~6 seconds 😴
}The Solution: Promise.all() with Await

We can fix this by using Promise.all(), which takes an array of promises and runs them in parallel. Then, we use await to wait for all of them to finish.
async function loadDashboard() {
try {
// Start all the fetches at the SAME TIME
const userPromise = fetchUser();
const postsPromise = fetchPosts();
const commentsPromise = fetchComments();
// Now wait for them all to finish
const results = await Promise.all([userPromise, postsPromise, commentsPromise]);
// results is an array: [userData, postsData, commentsData]
console.log("Dashboard data:", results);
// Total time: ~2 seconds (because they ran in parallel) 🚀
} catch (error) {
// If ANY ONE of the promises fails, the error lands here
console.error("Failed to load dashboard:", error);
}
}In this pattern, you get the best of both worlds. You use Promise.all() to handle the complex, high-performance task of running things in parallel. Then, you use the clean, readable async/await syntax to handle the results. This is the mark of a true JavaScript pro.
Common Pitfalls and How to Avoid Them
As you start using these patterns, here are a few common mistakes to watch out for:
- Forgetting
awaitin anasyncfunction: If you forgetawait, you won’t get the result. You’ll just get the promise object itself (e.g.,Promise {<pending>}). - Using
awaitoutside anasyncfunction: This will throw a syntax error. You can only useawaitinside a function marked with theasynckeyword. - The loop mistake: As we saw above, using
awaitinside afororforEachloop can make your code run slowly if the operations are independent. UsePromise.all()for independent tasks. - Forgetting error handling: Always wrap your
awaitlogic in atry...catchblock, and always add a.catch()to your promise chains. Unhandled promise rejections can lead to hard-to-find bugs.
Conclusion: You’ve Got This!
So, let’s recap. Asynchronous programming is a must in JavaScript, and it’s nothing to be scared of.
- Promises are like a receipt. They are the foundation. They give you a guarantee of a future value and let you handle it with
.then()and.catch(). They are great for chaining and are essential for working with modern APIs. - Async/Await is the beautiful syntax built on top of promises. It makes your asynchronous code look and feel synchronous, which is a huge win for readability and debugging. It’s perfect for most day-to-day tasks where you need to do things step by step.
- The real superpower comes when you combine them, using
Promise.all()inside anasyncfunction to run independent tasks in parallel for maximum performance.
Don’t feel like you have to master it all overnight. Start by playing with fetch, which returns a promise. Log the result. Then try rewriting it with async/await. The more you use it, the more natural it will feel. Happy coding!
Frequently Asked Questions (FAQs)
1. Is Async/Await a replacement for Promises?
No, not at all. async/await is built on top of Promises. Think of Promises as the engine and async/await as the steering wheel. You need the engine to make the car work, but the steering wheel makes it much easier to drive.
2. Can I use await for any function?
No, you can only use await for functions that return a Promise. If you try to await a regular function that returns a string or number, it will still work (it will treat it as a resolved promise), but it’s not its intended use.
3. Which one is faster, Promises or Async/Await?
In terms of raw execution speed, they are virtually identical because async/await is just syntactic sugar. The real performance difference comes from how you use them. Using sequential awaits for independent tasks will be slower than using Promise.all().
4. How do I handle errors with multiple await statements?
You wrap them in a try...catch block. The catch block will run if any of the awaited promises inside the try block reject. If you need more granular control (like handling errors for each step separately), you can use multiple try...catch blocks or fall back to adding .catch() to individual promises.
5. I’m confused. Should I use .catch() or try...catch?
If you’re writing code with .then() chains, use .catch() at the end. If you’re writing code with async/await, use try...catch. It’s generally considered a best practice to stick to one style within a single function for consistency and readability.
