- Published on
Today I Learnt: JavaScript Async/Await Patterns
- Authors

- Name
- Mehdi Akiki
Reference
Today I Learnt: JavaScript Async/Await Patterns
JavaScript's async/await syntax, introduced in ES2017, provides a cleaner way to work with asynchronous code compared to promise chains. It makes asynchronous code look and behave more like synchronous code.
Basic Syntax
// Async function declaration
async function fetchUserData(userId) {
// Await pauses execution until the promise resolves
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
return userData;
}
// Async arrow function
const fetchUserData = async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
};
// Usage
async function displayUser() {
try {
const user = await fetchUserData(123);
console.log("User:", user);
} catch (error) {
console.error("Error:", error);
}
}
Error Handling Patterns
Try/Catch Blocks
async function safeApiCall(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("API call failed:", error.message);
throw error; // Re-throw to let caller handle it
}
}
Multiple Async Operations
async function processUserData(userId) {
try {
// Sequential execution
const user = await fetchUser(userId);
const posts = await fetchUserPosts(userId);
const comments = await fetchUserComments(userId);
return {
user,
posts,
comments,
stats: {
totalPosts: posts.length,
totalComments: comments.length,
},
};
} catch (error) {
// Handle errors from any of the operations
console.error("Failed to process user data:", error);
return null;
}
}
Parallel Execution Patterns
Promise.all for Independent Operations
async function fetchDashboardData(userId) {
try {
// Start all requests simultaneously
const [user, posts, notifications, settings] = await Promise.all([
fetchUser(userId),
fetchUserPosts(userId),
fetchNotifications(userId),
fetchUserSettings(userId),
]);
return {
user,
posts,
notifications,
settings,
};
} catch (error) {
console.error("Failed to fetch dashboard data:", error);
throw error;
}
}
Promise.allSettled for Partial Failures
async function fetchOptionalData(userId) {
const results = await Promise.allSettled([
fetchUserProfile(userId), // Critical
fetchUserStats(userId), // Optional
fetchUserPreferences(userId), // Optional
fetchUserActivity(userId), // Optional
]);
const data = {};
results.forEach((result, index) => {
if (result.status === "fulfilled") {
// Map results to data object
switch (index) {
case 0:
data.profile = result.value;
break;
case 1:
data.stats = result.value;
break;
case 2:
data.preferences = result.value;
break;
case 3:
data.activity = result.value;
break;
}
} else {
console.warn(`Optional data fetch failed:`, result.reason);
}
});
return data;
}
Promise.race for Timeouts
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === "AbortError") {
throw new Error("Request timed out");
}
throw error;
}
}
// Using Promise.race
async function fetchWithTimeoutRace(url, timeoutMs = 5000) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Request timed out")), timeoutMs);
});
return Promise.race([fetch(url), timeoutPromise]);
}
Advanced Patterns
Async Iterators and Generators
// Async generator for paginated data
async function* fetchPaginatedData(url, pageSize = 10) {
let page = 1;
let hasMore = true;
while (hasMore) {
try {
const response = await fetch(`${url}?page=${page}&limit=${pageSize}`);
const data = await response.json();
if (data.items.length === 0) {
hasMore = false;
} else {
yield data.items;
page++;
}
} catch (error) {
console.error("Failed to fetch page:", error);
hasMore = false;
}
}
}
// Usage
async function processAllData() {
const dataGenerator = fetchPaginatedData("/api/data");
for await (const pageData of dataGenerator) {
console.log("Processing page:", pageData);
// Process each page
}
}
Async Function Composition
// Higher-order function for async operations
function withRetry(fn, maxRetries = 3, delay = 1000) {
return async (...args) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn(...args);
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
console.warn(`Attempt ${attempt} failed, retrying...`);
await new Promise((resolve) => setTimeout(resolve, delay * attempt));
}
}
};
}
// Usage
const reliableFetch = withRetry(fetchUserData);
const userData = await reliableFetch(123);
Async Context and Cleanup
class AsyncResourceManager {
constructor() {
this.resources = [];
}
async addResource(resource) {
this.resources.push(resource);
return resource;
}
async cleanup() {
const cleanupPromises = this.resources.map(async (resource) => {
if (typeof resource.cleanup === "function") {
await resource.cleanup();
}
});
await Promise.all(cleanupPromises);
this.resources = [];
}
}
// Usage with async context
async function processWithCleanup() {
const manager = new AsyncResourceManager();
try {
const dbConnection = await manager.addResource(await connectToDatabase());
const fileHandle = await manager.addResource(await openFile("data.txt"));
// Process data
const data = await processData(dbConnection, fileHandle);
return data;
} finally {
await manager.cleanup();
}
}
Performance Optimization
Concurrent vs Sequential Execution
// Sequential (slower)
async function slowProcess(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
// Concurrent (faster)
async function fastProcess(items) {
const promises = items.map((item) => processItem(item));
return Promise.all(promises);
}
// Batching for large datasets
async function batchProcess(items, batchSize = 10) {
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;
}
Memory-Efficient Streaming
// Process large files without loading everything into memory
async function processLargeFile(filePath) {
const fileHandle = await fs.promises.open(filePath, "r");
const stream = fileHandle.createReadStream();
return new Promise((resolve, reject) => {
let processedLines = 0;
let totalSize = 0;
stream.on("data", async (chunk) => {
try {
// Pause stream while processing
stream.pause();
const lines = chunk.toString().split("\n");
for (const line of lines) {
if (line.trim()) {
await processLine(line);
processedLines++;
}
}
totalSize += chunk.length;
// Resume stream
stream.resume();
} catch (error) {
stream.destroy();
reject(error);
}
});
stream.on("end", () => {
fileHandle.close();
resolve({ processedLines, totalSize });
});
stream.on("error", (error) => {
fileHandle.close();
reject(error);
});
});
}
Error Handling Strategies
Graceful Degradation
async function loadDashboardData(userId) {
const result = {
user: null,
posts: [],
notifications: [],
error: null,
};
try {
// Critical data - fail if this fails
result.user = await fetchUser(userId);
} catch (error) {
result.error = "Failed to load user data";
return result;
}
// Non-critical data - continue even if these fail
const [postsResult, notificationsResult] = await Promise.allSettled([
fetchUserPosts(userId),
fetchNotifications(userId),
]);
if (postsResult.status === "fulfilled") {
result.posts = postsResult.value;
}
if (notificationsResult.status === "fulfilled") {
result.notifications = notificationsResult.value;
}
return result;
}
Circuit Breaker Pattern
class CircuitBreaker {
constructor(failureThreshold = 5, recoveryTimeout = 60000) {
this.failureThreshold = failureThreshold;
this.recoveryTimeout = recoveryTimeout;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN
}
async execute(asyncFn, ...args) {
if (this.state === "OPEN") {
if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
this.state = "HALF_OPEN";
} else {
throw new Error("Circuit breaker is OPEN");
}
}
try {
const result = await asyncFn(...args);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = "CLOSED";
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = "OPEN";
}
}
}
// Usage
const breaker = new CircuitBreaker();
async function safeApiCall() {
return breaker.execute(() => fetch("/api/data"));
}
Testing Async Code
// Testing async functions
describe("Async Operations", () => {
test("should handle successful async operation", async () => {
const result = await fetchUserData(123);
expect(result).toHaveProperty("id", 123);
});
test("should handle async errors", async () => {
await expect(fetchUserData(999)).rejects.toThrow("User not found");
});
test("should handle timeouts", async () => {
const slowOperation = () => new Promise((resolve) => setTimeout(resolve, 10000));
await expect(fetchWithTimeout(slowOperation, 1000)).rejects.toThrow("Request timed out");
});
test("should process parallel operations", async () => {
const startTime = Date.now();
await fetchDashboardData(123);
const duration = Date.now() - startTime;
// Should be faster than sequential execution
expect(duration).toBeLessThan(1000);
});
});
Best Practices
- Always Use Try/Catch: Wrap async operations in try/catch blocks
- Prefer Promise.all for Independent Operations: Run independent async operations in parallel
- Use Promise.allSettled for Robustness: Handle partial failures gracefully
- Implement Timeouts: Prevent hanging operations
- Consider Memory Usage: Be mindful of concurrent operations
- Test Async Code: Write comprehensive tests for async functions
- Handle Cleanup: Properly clean up resources in async operations
- Use Descriptive Variable Names: Make async code self-documenting
Async/await syntax has revolutionized asynchronous programming in JavaScript, making it more readable and maintainable. Mastering these patterns will help you write more efficient, reliable, and performant asynchronous code.