Async/Await
Table of Contents
Async/await is syntactic sugar on top of Promises. It lets you write asynchronous code that reads like synchronous code — no more .then() chains. Under the hood, it’s still promises, but the code is dramatically easier to read and reason about.
The Basics
Mark a function as async, then use await inside it to pause until a promise resolves:
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
}
// Calling an async function returns a promise
getUser(1).then(user => console.log(user.name));
Compare this to the promise chain version:
function getUser(id) {
return fetch(`/api/users/${id}`)
.then(response => response.json());
}
For simple cases, they’re similar. The advantage of async/await becomes clear with more complex flows.
Sequential Operations
When each step depends on the previous one, async/await shines:
async function getOrderSummary(userId) {
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
const ordersResponse = await fetch(`/api/orders?userId=${user.id}`);
const orders = await ordersResponse.json();
const latestOrder = orders[0];
const detailsResponse = await fetch(`/api/orders/${latestOrder.id}`);
const details = await detailsResponse.json();
return {
userName: user.name,
orderCount: orders.length,
latestOrderTotal: details.total
};
}
The same thing with .then() chains would be much harder to follow.
Error Handling with try/catch
Use standard try/catch blocks — no more .catch() callbacks:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error("Failed to fetch user:", error.message);
return null;
}
}
try/catch with multiple operations
async function processPayment(orderId, paymentDetails) {
try {
const order = await getOrder(orderId);
const validation = await validatePayment(paymentDetails);
const result = await chargeCard(validation.token, order.total);
await sendConfirmationEmail(order.email, result.transactionId);
return { success: true, transactionId: result.transactionId };
} catch (error) {
// Any failure in the chain lands here
await logPaymentError(orderId, error);
return { success: false, error: error.message };
} finally {
// Runs regardless of success or failure
releasePaymentLock(orderId);
}
}
Parallel Execution
A common mistake: using await sequentially when operations are independent.
// Bad — these run one after another (slow)
async function getDashboardData() {
const users = await fetch("/api/users").then(r => r.json());
const posts = await fetch("/api/posts").then(r => r.json());
const stats = await fetch("/api/stats").then(r => r.json());
return { users, posts, stats };
}
// Good — all three run in parallel (fast)
async function getDashboardData() {
const [users, posts, stats] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
fetch("/api/stats").then(r => r.json())
]);
return { users, posts, stats };
}
If the three requests each take 200ms:
- Sequential: ~600ms total
- Parallel: ~200ms total
Use await Promise.all() whenever operations don’t depend on each other.
Loops and Async
Sequential processing (one at a time)
async function processItems(items) {
const results = [];
for (const item of items) {
const result = await processItem(item); // waits for each one
results.push(result);
}
return results;
}
Use this when order matters or when you’d overwhelm a server with parallel requests.
Parallel processing (all at once)
async function processItems(items) {
const results = await Promise.all(
items.map(item => processItem(item))
);
return results;
}
Use this when items are independent and you want maximum speed.
Controlled concurrency (batches)
async function processInBatches(items, batchSize = 5) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(item => processItem(item))
);
results.push(...batchResults);
}
return results;
}
forEach doesn’t work with async/await — it doesn’t wait for the promises. Always use for...of for sequential async loops, or Promise.all with .map() for parallel.
Async Arrow Functions
// Named async function
async function fetchData() { /* ... */ }
// Async arrow function
const fetchData = async () => { /* ... */ };
// Async method in an object
const api = {
async getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
};
// Async IIFE (immediately invoked)
(async () => {
const data = await fetchData();
console.log(data);
})();
Real-World Patterns
Fetch wrapper with error handling
async function api(endpoint, options = {}) {
const baseUrl = "https://api.example.com";
const response = await fetch(`${baseUrl}${endpoint}`, {
headers: { "Content-Type": "application/json" },
...options
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
// Usage
async function createUser(userData) {
try {
const user = await api("/users", {
method: "POST",
body: JSON.stringify(userData)
});
return user;
} catch (error) {
console.error("Failed to create user:", error.message);
throw error;
}
}
Retry with exponential backoff
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Loading state management
async function loadData(setState) {
setState({ loading: true, error: null });
try {
const data = await api("/data");
setState({ loading: false, data, error: null });
} catch (error) {
setState({ loading: false, data: null, error: error.message });
}
}
Common Mistakes
Forgetting await
// Bug — response is a Promise, not the actual response
async function getUser() {
const response = fetch("/api/user"); // missing await!
const user = response.json(); // TypeError: response.json is not a function
}
Using await at the top level (without a module)
// This only works in ES modules or modern environments
const data = await fetch("/api/data"); // SyntaxError in non-module scripts
// Workaround for non-module scripts
(async () => {
const data = await fetch("/api/data");
})();
Swallowing errors silently
// Bad — error disappears
async function getData() {
try {
return await fetch("/api/data").then(r => r.json());
} catch (error) {
// silently returns undefined
}
}
// Good — handle or re-throw
async function getData() {
try {
return await fetch("/api/data").then(r => r.json());
} catch (error) {
console.error("getData failed:", error);
throw error; // let the caller handle it
}
}
When to Use What
| Situation | Approach |
|---|---|
| Simple single async call | Either .then() or async/await |
| Multiple sequential steps | async/await (much cleaner) |
| Parallel independent operations | await Promise.all([...]) |
| Error handling with recovery | try/catch in async function |
| Event handlers | async callback: btn.addEventListener("click", async () => {...}) |
| Top-level script | Async IIFE or ES module top-level await |