Async/Await Patterns
Advanced async/await usage, error handling, parallel execution, and best practices
Async/Await Fundamentals
async/await is syntactic sugar over Promises that makes asynchronous code look and behave
more like synchronous code. It's the preferred way to write async JavaScript in modern applications.
Key Points
- async — Declares a function that returns a Promise
- await — Pauses execution until Promise resolves
- Return value — Automatically wrapped in Promise.resolve()
- Errors — Thrown errors become rejected promises
Basic Syntax
// Async function declaration
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
}
// Arrow function
const fetchUser = async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
// Using the function
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error(error));
// Or with await (inside another async function)
const user = await fetchUser(1);
Error Handling with try/catch
async function fetchData() {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Fetch failed:", error.message);
throw error; // Re-throw if needed
} finally {
console.log("Cleanup code runs regardless");
}
}
// Multiple operations in one try block
async function processUser(id) {
try {
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
} catch (error) {
// Catches errors from any of the three fetches
console.error("Error in pipeline:", error);
return null;
}
}
Parallel Execution
Await multiple promises simultaneously for better performance:
// ❌ Sequential (slow) - each waits for the previous
async function sequential() {
const users = await fetchUsers(); // Wait 1s
const posts = await fetchPosts(); // Wait 1s
const comments = await fetchComments(); // Wait 1s
// Total: ~3s
}
// ✅ Parallel (fast) - all run at the same time
async function parallel() {
const [users, posts, comments] = await Promise.all([
fetchUsers(), // Start immediately
fetchPosts(), // Start immediately
fetchComments() // Start immediately
]);
// Total: ~1s (slowest request)
}
// ✅ Parallel with error handling for each
async function parallelWithFallbacks() {
const results = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return results.map(r =>
r.status === "fulfilled" ? r.value : null
);
}
Sequential vs Parallel Decision
// Use SEQUENTIAL when operations depend on each other
async function dependent() {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id); // Needs user.id
const comments = await fetchComments(posts); // Needs posts
return { user, posts, comments };
}
// Use PARALLEL when operations are independent
async function independent() {
const [user, settings, notifications] = await Promise.all([
fetchUser(), // Independent
fetchSettings(), // Independent
fetchNotifications() // Independent
]);
return { user, settings, notifications };
}
// HYBRID: Mix both approaches
async function hybrid(userId) {
const user = await fetchUser(userId);
// These depend on user but not on each other
const [posts, followers, settings] = await Promise.all([
fetchPosts(user.id),
fetchFollowers(user.id),
fetchSettings(user.id)
]);
return { user, posts, followers, settings };
}
Loops with Async/Await
// Sequential loop - one at a time
async function processSequentially(ids) {
const results = [];
for (const id of ids) {
const result = await processItem(id);
results.push(result);
}
return results;
}
// Parallel loop - all at once
async function processInParallel(ids) {
const promises = ids.map(id => processItem(id));
return Promise.all(promises);
}
// Controlled concurrency - batch processing
async function processBatches(ids, batchSize = 5) {
const results = [];
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(id => processItem(id))
);
results.push(...batchResults);
}
return results;
}
// ⚠️ forEach doesn't work with await
// ❌ BAD - won't wait for promises
ids.forEach(async (id) => {
await processItem(id); // Doesn't wait!
});
// ✅ GOOD - use for...of
for (const id of ids) {
await processItem(id);
}
Advanced Patterns
// Retry pattern
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fetch(url);
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.pow(2, attempt) * 1000;
console.log(`Retry ${attempt} in ${delay}ms`);
await new Promise(r => setTimeout(r, delay));
}
}
}
// Timeout pattern
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { signal: controller.signal });
return response;
} finally {
clearTimeout(timeoutId);
}
}
// Cancellable async operation
function createCancellableRequest(url) {
const controller = new AbortController();
const promise = fetch(url, { signal: controller.signal });
return {
promise,
cancel: () => controller.abort()
};
}
const { promise, cancel } = createCancellableRequest("/api/data");
// Later: cancel();
Async in Class Methods
class UserService {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async getUser(id) {
const response = await fetch(`${this.baseUrl}/users/${id}`);
return response.json();
}
async createUser(userData) {
const response = await fetch(`${this.baseUrl}/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData)
});
return response.json();
}
// Getter can't be async, but can return a Promise
get currentUser() {
return this.getUser("me");
}
}
const service = new UserService("/api");
const user = await service.getUser(1);
⚠️ Common Mistakes
- Sequential when parallel is possible: Don't await unnecessarily in sequence
- Using forEach with async: Use for...of or Promise.all() instead
- Not handling errors: Always use try/catch or .catch()
- Forgetting await: Results in working with Promise objects, not values
- await in non-async function: Only works inside async functions
💡 Key Takeaways
- • async/await makes async code readable and maintainable
- • Use try/catch for error handling
- • Use Promise.all() for independent parallel operations
- • Use for...of for sequential async loops
- • Consider AbortController for cancellation and timeouts
- • Always handle errors to prevent unhandled rejections