The Relationship Between Them
A common misconception is that async/await and Promises are two separate systems for handling asynchronous code. They're not. async/await is syntactic sugar built directly on top of the Promise API. Every async function returns a Promise, and await is just a more readable way to consume one.
Understanding this relationship is the key to using both effectively — and to debugging them when they go wrong.
How Promises Work
A Promise represents a value that will be available at some point in the future. It can be in one of three states: pending, fulfilled, or rejected. You chain .then() for success and .catch() for errors:
fetch("/api/user")
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
This works well for simple chains, but becomes harder to read when you need to share data between multiple .then() blocks or handle complex branching logic.
How Async/Await Works
The same fetch call, written with async/await:
async function getUser() {
try {
const response = await fetch("/api/user");
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
It reads like synchronous code, which makes it significantly easier to follow — especially when you have multiple sequential async operations that depend on each other's results.
Where Promises Still Win
Despite the readability benefits of async/await, there are situations where Promise methods are cleaner or more capable:
Running Operations in Parallel
Promise.all() lets you kick off multiple async operations at the same time and wait for all of them to complete:
const [user, posts, comments] = await Promise.all([
fetchUser(id),
fetchPosts(id),
fetchComments(id),
]);
If you await each call sequentially, you're waiting for each one to finish before the next starts — much slower when the operations are independent.
Other Useful Promise Methods
Promise.allSettled()— waits for all promises, regardless of whether any rejectedPromise.race()— resolves or rejects as soon as the first promise settlesPromise.any()— resolves with the first fulfilled promise, ignoring rejections
Common Mistakes to Avoid
- Forgetting
await— you get back a Promise object instead of the resolved value. TypeScript can catch this. - Awaiting in a loop unnecessarily — use
Promise.all(map(...))instead offor...ofwithawaitwhen operations can run in parallel. - Missing error handling — an unawaited rejected promise can silently fail. Always wrap
asyncfunctions intry/catchor handle the returned Promise's.catch(). - Making non-async functions
asyncunnecessarily — it wraps the return value in a Promise for no reason and adds confusion.
The Practical Recommendation
Use async/await as your default for writing async code — it's more readable and easier to reason about. Reach for Promise methods like Promise.all() and Promise.allSettled() when you need to coordinate multiple async operations. You'll often use both in the same function, and that's perfectly fine — they're designed to work together.