Async/Await vs Promises in JavaScript: Which Should You Use?
Async/Await vs Promises in JavaScript: A Complete Guide
Asynchronous programming is one of the most important topics in modern JavaScript development. Whether you’re building a web app, mobile application, or integrating APIs, you’ll often deal with operations that don’t finish instantly, such as fetching data or reading files.
This is where Promises and Async/Await come into play. In this guide, we’ll dive deep into Promises vs Async/Await, explore how they work, compare their syntax, look at real-world examples, and discuss when to use each.
Introduction
Why Asynchronous Programming Matters in JavaScript
JavaScript runs on a single thread, meaning one task executes at a time. If you block this thread with a long-running task (like fetching API data), the application freezes until it’s done.
Asynchronous programming allows JavaScript to:
- Perform multiple tasks without blocking.
- Keep the UI smooth and responsive.
- Handle heavy tasks like API calls and file I/O efficiently.
Example:
Imagine a weather app fetching live data. Without async code, the app would hang until the response arrives. With Promises or Async/Await, the app remains interactive while the data loads in the background.
The Problem with Callbacks
Before Promises, JavaScript developers relied heavily on callbacks to handle asynchronous operations.
A callback is simply a function passed as an argument to another function, which gets executed once the task is completed.
Example: Basic Callback
function fetchData(callback) {
setTimeout(() => {
callback("Data received!");
}, 2000);
}
fetchData(function(result) {
console.log(result);
});
👉 Here, fetchData waits 2 seconds, then runs the callback function to log "Data received!".
Callback Hell
Callbacks are fine for simple tasks, but they quickly become unmanageable when multiple asynchronous operations depend on each other. This leads to “callback hell”—deeply nested, pyramid-shaped code that’s difficult to read and maintain.
Example: Callback Hell
getUser(function(user) {
getPosts(user.id, function(posts) {
getComments(posts[0].id, function(comments) {
getLikes(comments[0].id, function(likes) {
console.log("Likes:", likes);
});
});
});
});
Here’s what happens:
getUser()is called.- With that result,
getPosts()is called. - Then
getComments(). - Finally,
getLikes().
What Are Promises in JavaScript?
A Promise is an object that represents the eventual completion (or failure) of an asynchronous task. Instead of returning a value right away, it gives a placeholder that will be filled later.
- Helps manage asynchronous tasks.
- Makes code more structured compared to callbacks.
- Provides
.then()and.catch()for handling results and errors.
It can be in one of the following three states:
Pending
- This is the initial state of a Promise.
- It means the asynchronous operation has started but is not yet finished.
- At this stage, the Promise has no final value (neither success nor failure).
- Example: waiting for data from an API call.
const promise = new Promise((resolve, reject) => {
// still waiting, nothing resolved or rejected yet
});
console.log(promise); // Promise { <pending> }
Fulfilled (Resolved)
- When the asynchronous task completes successfully, the Promise changes from pending → fulfilled.
- A value (the result of the operation) is returned and passed to the
.then()handler. - Example: API data successfully retrieved.
const promise = Promise.resolve("Data loaded!");
promise.then(result => console.log(result)); // Output: Data loaded!
Rejected
- When the asynchronous task fails (due to an error, network issue, or invalid input), the Promise moves from pending → rejected.
- An error reason is provided, which can be handled using
.catch(). - Example: failed API request.
const promise = Promise.reject("Network error!");
promise.catch(error => console.error(error)); // Output: Network error!
Flowchart: Promise States
┌─────────┐
│ Pending │
└────┬────┘
│
┌────▼────┐ ┌──────────┐
│ Fulfilled│ OR → │ Rejected │
└─────────┘ └──────────┘
Example: Basic Promise
let myPromise = new Promise((resolve, reject) => {
let isSuccess = true;
if (isSuccess) {
resolve("Task completed successfully!");
} else {
reject("Something went wrong!");
}
});
myPromise
.then(result => console.log(result))
.catch(error => console.error(error));
Chaining with .then() and .catch()
One of the biggest advantages of Promises is that they allow you to chain multiple asynchronous operations together in a clean, linear way.
1. .then()
- Used to handle the successful result of a Promise.
- Each
.then()returns a new Promise, allowing chaining. - The value returned in one
.then()can be passed to the next.then().
fetch("<https://jsonplaceholder.typicode.com/posts/1>")
.then(res => res.json()) // first then → convert response to JSON
.then(data => console.log(data)) // second then → access parsed data
2. .catch()
- Used to handle errors if a Promise is rejected.
- Can be placed at the end of a chain to catch errors from any step.
fetch("<https://jsonplaceholder.typicode.com/invalid-url>")
.then(res => res.json())
.then(data => console.log(data))
.catch(error => console.error("Something went wrong:", error));
3. Chaining Example
Here’s a full example with multiple .then() calls and one .catch():
fetch("<https://jsonplaceholder.typicode.com/users/1>")
.then(res => res.json()) // step 1: parse user
.then(user => fetch(`/posts?userId=${user.id}`)) // step 2: fetch posts
.then(res => res.json()) // step 3: parse posts
.then(posts => console.log("Posts:", posts)) // step 4: log posts
.catch(err => console.error("Error:", err)); // handle any errors
What Is Async/Await in JavaScript?
Async/Await, introduced in ES2017 (ES8), is a feature built on top of Promises.
It doesn’t replace Promises but provides a cleaner, more readable syntax for working with them.
Think of it as writing asynchronous code that looks synchronous.
Syntactic Sugar over Promises
- An
asyncfunction always returns a Promise, even if you don’t explicitly return one. - Inside an
asyncfunction, you can use theawaitkeyword, which pauses execution until the Promise resolves. - This helps avoid chaining
.then()calls, making code easier to read.
How Async Functions Work
async function example() {
return "Hello!";
}
example().then(msg => console.log(msg)); // Outputs: Hello!
Explanation:
- The function
exampleis declared withasync. - Even though it returns a simple string
"Hello!", JavaScript wraps it in a Promise. - So calling
example()actually returns a Promise, which is why we can use.then()to get the value.
Using await for Cleaner Code
async function getData() {
const response = await fetch("<https://jsonplaceholder.typicode.com/users/1>");
const user = await response.json();
console.log("User:", user.name);
}
getData();
Explanation:
- The
awaitkeyword tells JavaScript: “wait here until this Promise is resolved.” fetch(...)returns a Promise → execution pauses until the response arrives.response.json()also returns a Promise → execution pauses again until data is parsed.- The code looks like normal synchronous code, but it’s actually asynchronous under the hood.
👉 This makes the code much easier to read and debug compared to .then() chaining.
Example: Converting a Promise Chain into Async/Await
Using Promises (chained with .then())
fetch("<https://jsonplaceholder.typicode.com/users/1>")
.then(res => res.json())
.then(user => console.log(user.name))
.catch(err => console.error(err));
- Each
.then()handles the next step. - Works fine, but for longer chains, it can get messy.
Using Async/Await (cleaner)
async function fetchUser() {
try {
const res = await fetch("<https://jsonplaceholder.typicode.com/users/1>");
const user = await res.json();
console.log(user.name);
} catch (err) {
console.error(err);
}
}
fetchUser();
- The same logic, but easier to read.
try...catchis used instead of.catch()for error handling, which feels more natural.- No chaining required → looks like step-by-step synchronous code.
Promises vs Async/Await: Key Differences
| Feature | Promises | Async/Await |
|---|---|---|
| Syntax | .then(), .catch() | try...catch with await |
| Readability | Nested chains can get messy | Looks synchronous & clean |
| Error Handling | .catch() | try...catch |
| Debugging | Stack traces harder | Cleaner stack traces |
| Parallel Execution | Promise.all() | await Promise.all() |
Example: Parallel Execution
// Sequential (slower)
await task1();
await task2();
// Parallel (faster)
await Promise.all([task1(), task2()]);
Code Examples
Fetching Data with Promises
One of the most common uses of Promises in JavaScript is fetching data from an API.
The fetch() function, built into modern browsers, returns a Promise that resolves to a Response object.
- First,
fetch()starts the HTTP request and immediately returns a Promise in the pending state. - When the response arrives successfully, the Promise becomes fulfilled, and you can handle it with
.then(). - If there’s a network error, the Promise becomes rejected, which you can handle with
.catch().
Example: Fetching Data with Promises
fetch("<https://jsonplaceholder.typicode.com/todos/1>")
.then(response => response.json()) // convert response to JSON
.then(data => console.log("Todo:", data)) // use the parsed data
.catch(error => console.error("Error:", error));
How this works step by step:
fetch(...)→ starts the request, returns a Promise..then(response => response.json())→ when fulfilled, converts the response into JSON (which itself returns another Promise)..then(data => console.log(data))→ logs the final data after JSON parsing..catch(error => ...)→ handles errors (like network failure).
Same Task with Async/Await
Earlier, we saw how to fetch data with Promises using .then() and .catch().
Now, we’ll do the same task with Async/Await, which makes the code look cleaner and easier to follow.
Using Promises
fetch("<https://jsonplaceholder.typicode.com/todos/1>")
.then(response => response.json())
.then(todo => console.log("Todo:", todo))
.catch(error => console.error("Error:", error));
- Each
.then()handles the next step. - Works fine, but as steps increase, the chain grows longer and harder to read.
Same Task with Async/Await
async function fetchTodo() {
try {
const res = await fetch("<https://jsonplaceholder.typicode.com/todos/1>");
const todo = await res.json();
console.log("Todo:", todo);
} catch (err) {
console.error("Error:", err);
}
}
fetchTodo();
Explanation
- Async Function
- The function is declared with
async, so it always returns a Promise.
- The function is declared with
- Await Fetch
await fetch(...)pauses execution until the HTTP response is received.
- Await JSON Parsing
await res.json()waits until the response body is fully read and converted into a JavaScript object.
- Console Output
- The result (
todo) is printed directly.
- The result (
- Error Handling with Try/Catch
- Any error in fetching or parsing is caught in the
catchblock.
- Any error in fetching or parsing is caught in the
Error Handling Comparison
Error handling is an important part of asynchronous programming. Both Promises and Async/Await provide ways to deal with errors, but the style is slightly different.
With Promises
- Errors are handled using
.catch(). - If any step in the chain fails (network error, parsing error, etc.), it jumps into
.catch().
fetch("<https://jsonplaceholder.typicode.com/invalid-url>")
.then(response => response.json())
.then(data => console.log("Data:", data))
.catch(error => console.error("Promise Error:", error));
👉 Here, .catch() will catch:
- Network failures
- Invalid JSON parsing
- Or anything thrown in previous
.then()
With Async/Await
- Errors are handled using
try...catch. - If any
awaited Promise rejects, execution jumps to thecatchblock.
async function fetchData() {
try {
const res = await fetch("<https://jsonplaceholder.typicode.com/invalid-url>");
const data = await res.json();
console.log("Data:", data);
} catch (error) {
console.error("Async/Await Error:", error);
}
}
fetchData();
👉 Here, try...catch makes error handling look like synchronous code.
When to Use Promises
- For concurrent tasks.
- When using libraries already promise-based.
- For lightweight chaining of async tasks.
When to Use Async/Await
- For sequential logic.
- To achieve readable synchronous-like flow.
- When you want easier debugging and error handling.
Performance Considerations
Do Async/Await Make Code Faster?
No. Both Promises and Async/Await run on the same engine mechanics. Async/Await is not faster—it’s just more readable.
Promise.all with Async/Await for Optimization
Promise.all with Async/Await allows you to run multiple asynchronous tasks in parallel instead of waiting for each one separately. This improves performance by fetching or processing data simultaneously, and await Promise.all([...]) waits until all promises are resolved (or rejected).
async function getUserAndPosts() {
const [userRes, postsRes] = await Promise.all([
fetch("<https://jsonplaceholder.typicode.com/users/1>"),
fetch("<https://jsonplaceholder.typicode.com/posts?userId=1>")
]);
const user = await userRes.json();
const posts = await postsRes.json();
console.log("User:", user);
console.log("Posts:", posts);
}
getUserAndPosts();
Common Mistakes to Avoid
- Forgetting try...catch in async/await.
async function badExample() {
const res = await fetch("invalid-url"); // crashes without try/catch
}
- Mixing
.then()andawaitunnecessarily.
// Wrong
await fetch(url).then(res => res.json());
// Correct
const res = await fetch(url);
const data = await res.json();
- Blocking code with multiple awaits in a loop.
// Wrong: Slow
for (let url of urls) {
const res = await fetch(url);
}
// Correct: Fast
await Promise.all(urls.map(url => fetch(url)));
Conclusion
Both Promises and Async/Await are essential for handling asynchronous code in JavaScript.
- Promises → Great for concurrency and chaining.
- Async/Await → Ideal for readable, step-by-step async code.
👉 The best approach is often to combine them depending on the situation.
FAQs
Q1. Can we use async/await without Promises?
No. Async/Await is built on top of Promises.
Q2. Is async/await better than Promises?
Not faster—just cleaner. Promises are still very important.
Q3. Does async/await replace callbacks?
Yes, in modern JavaScript, async/await helps you avoid callback hell.


