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
async
function always returns a Promise, even if you don’t explicitly return one. - Inside an
async
function, you can use theawait
keyword, 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
example
is 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
await
keyword 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...catch
is 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
catch
block.
- 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
await
ed Promise rejects, execution jumps to thecatch
block.
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()
andawait
unnecessarily.
// 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.