JavaScript Async/Await
async/await makes asynchronous JavaScript code look and behave like synchronous code. It's built on top of Promises and provides a cleaner, more readable way to handle async operations.
What is Async/Await?
Async/await is syntactic sugar over Promises. The async keyword declares a function as asynchronous, and await pauses execution until a Promise resolves.
// Declare an async function
async function fetchData() {
// await pauses until the promise resolves
const response = await fetch('/api/data');
const data = await response.json();
return data;
}
// Call it (returns a Promise)
fetchData().then(data => console.log(data));async function always returns a Promise. If you return a value, it's automatically wrapped in Promise.resolve(). Why Use Async/Await?
Before async/await, we used Promise chains with .then(). While Promises are powerful, chaining them can become hard to read. Compare these two approaches:
// With Promises (chaining)
fetch('/api/user')
.then(response => response.json())
.then(user => fetch(`/api/posts/${user.id}`))
.then(response => response.json())
.then(posts => console.log(posts))
.catch(error => console.error(error));
// With async/await (cleaner)
async function getUserPosts() {
const response = await fetch('/api/user');
const user = await response.json();
const postsResponse = await fetch(`/api/posts/${user.id}`);
const posts = await postsResponse.json();
console.log(posts);
}Try It Yourself
Run this example to see async/await in action:
// Simulating an API call with a delay
function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: "Alice", email: "[email protected]" });
}, 1000);
});
}
// Using async/await
async function getUser() {
console.log("Fetching user...");
const user = await fetchUser(1);
console.log("User fetched:", user);
return user;
}
getUser();Callbacks vs Promises vs Async/Await
| Feature | Callbacks | Promises | Async/Await |
|---|---|---|---|
| Readability | Poor | Good | Excellent |
| Error Handling | Difficult | .catch() | try/catch |
| Chaining | Callback Hell | .then() chains | Sequential code |
| Debugging | Hard | Moderate | Easy |
| Return Values |
Error Handling
Use try/catch blocks to handle errors in async functions. This works just like synchronous error handling.
async function fetchUser() {
try {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('Failed to fetch user:', error.message);
throw error; // Re-throw if you want calling code to handle it
}
}Try this interactive example that demonstrates error handling:
// Simulating an API that might fail
function fetchData(shouldFail) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error("Network error: Failed to fetch"));
} else {
resolve({ data: "Success!" });
}
}, 500);
});
}
// Handling errors with try/catch
async function getData() {
try {
console.log("Attempting to fetch data...");
const result = await fetchData(true); // This will fail
console.log("Result:", result);
} catch (error) {
console.error("Caught error:", error.message);
} finally {
console.log("Cleanup: fetch attempt complete");
}
}
getData();.catch() when calling async functions. Parallel Execution
Multiple await statements run sequentially. To run operations in parallel, use Promise.all():
// Run multiple requests in parallel
async function fetchDashboard() {
const [user, posts, notifications] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/notifications').then(r => r.json())
]);
return { user, posts, notifications };
}Promise.allSettled
Unlike Promise.all() which rejects if any promise rejects, Promise.allSettled() waits for all promises and tells you which succeeded or failed:
// Get results even if some fail
async function fetchAllData() {
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/will-fail')
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`Request ${i} succeeded`);
} else {
console.log(`Request ${i} failed: ${result.reason}`);
}
});
}See the performance difference between sequential and parallel execution:
// Simulating multiple API calls
function fetchUser() {
return new Promise(resolve => {
setTimeout(() => resolve({ name: "Alice" }), 1000);
});
}
function fetchPosts() {
return new Promise(resolve => {
setTimeout(() => resolve([{ title: "Post 1" }, { title: "Post 2" }]), 800);
});
}
function fetchComments() {
return new Promise(resolve => {
setTimeout(() => resolve([{ text: "Great!" }]), 600);
});
}
// Sequential (slow) - takes ~2.4 seconds
async function fetchSequential() {
console.time("Sequential");
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();
console.timeEnd("Sequential");
console.log("Sequential result:", { user, posts, comments });
}
// Parallel (fast) - takes ~1 second
async function fetchParallel() {
console.time("Parallel");
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
console.timeEnd("Parallel");
console.log("Parallel result:", { user, posts, comments });
}
// Run both to compare
fetchSequential();
setTimeout(fetchParallel, 3000);Async/Await in Loops
Be careful when using async/await in loops. The behavior differs between for...of and array methods like forEach.
// Sequential - each iteration waits for previous
async function processSequential(items) {
for (const item of items) {
await processItem(item); // Waits before next iteration
}
}
// Parallel - all items processed at once
async function processParallel(items) {
await Promise.all(items.map(item => processItem(item)));
}Common Mistakes
1. Using forEach with async/await
forEach doesn't understand async functions. The iterations don't wait for each other, and the code after forEach runs before the async operations complete. // WRONG: forEach doesn't wait for async
async function wrong() {
const items = [1, 2, 3];
items.forEach(async (item) => {
await processItem(item); // These run in parallel, not sequentially!
});
console.log('Done'); // This runs before items are processed!
}
// CORRECT: Use for...of for sequential processing
async function correct() {
const items = [1, 2, 3];
for (const item of items) {
await processItem(item);
}
console.log('Done'); // This runs after all items are processed
}2. Forgetting to await
await, you get a Promise object instead of the resolved value. This is a common source of bugs. 3. Sequential when parallel is possible
Promise.all() to run them in parallel. This can dramatically improve performance. Best Practices
Promise.all() instead of sequential awaits. for...of loops when you need sequential async iteration. Never use forEach with async functions. Test Your Knowledge
Test Your Knowledge
5 questionsWhat does the async keyword do to a function?
What happens when you use await outside an async function?
How do you run multiple async operations in parallel?
What is the best way to handle errors in async/await?
What does Promise.all() return if one promise rejects?
Practice Exercises
Challenge 1: Convert Promise Chain
Convert the promise chain to use async/await syntax. The function should fetch a user and then fetch their posts.
// Challenge: Convert this promise chain to async/await
// The function should fetch a user, then fetch their posts
function getUserWithPosts(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => {
return fetch(`/api/users/${userId}/posts`)
.then(response => response.json())
.then(posts => {
return { user, posts };
});
});
}
// Your async/await version here:
async function getUserWithPostsAsync(userId) {
// TODO: Convert the above to async/await
}
Challenge 2: Parallel Fetch with Error Handling
Fetch multiple URLs in parallel, but return null for any requests that fail instead of throwing an error.
// Challenge: Fetch 3 URLs in parallel and handle errors
// Return an array of results, with null for any that failed
const urls = [
'https://api.example.com/users',
'https://api.example.com/posts',
'https://api.example.com/invalid' // This one might fail
];
async function fetchAllSafely(urls) {
// TODO: Fetch all URLs in parallel
// Return results array where failed requests return null
// Hint: Use Promise.allSettled or try/catch with Promise.all
}
Frequently Asked Questions
Can I use await at the top level of a file?
Yes, but only in ES modules (files with .mjs extension or "type": "module" in package.json). This is called "top-level await" and was added in ES2022.
What's the difference between Promise.all and Promise.allSettled?
Promise.all() rejects immediately if any promise rejects. Promise.allSettled() waits for all promises and returns an array of results, telling you which succeeded and which failed.
Is async/await slower than Promises?
No, async/await compiles down to Promises. The performance is identical. The only difference is readability and developer experience.
How do I cancel an async operation?
Use AbortController. Create a controller, pass its signal to fetch or other APIs, and call controller.abort() to cancel. The promise will reject with an AbortError.
Summary
You've learned how to use async/await to write cleaner asynchronous JavaScript:
- async - Declares a function that returns a Promise
- await - Pauses execution until a Promise resolves
- try/catch - Handles errors in async functions
- Promise.all() - Runs multiple operations in parallel
Async/await makes asynchronous code much easier to read and maintain. Use it whenever you're working with Promises, API calls, or any asynchronous operations.