Async/Await
Async/await is a modern JavaScript feature that provides a more elegant way to work with asynchronous operations. It builds on promises and makes asynchronous code look and behave more like synchronous code. This guide will explore how async/await works and demonstrate its practical applications.
What is Async/Await?
Async/await consists of two keywords: - async
: Declares an asynchronous function that returns a promise - await
: Pauses execution until a promise is resolved
Basic Syntax and Usage
async function fetchUserData() {
try {
const response = await fetch('https://api.example.com/user');
const data = await response.json();
return data;
catch (error) {
} console.error('Error fetching user data:', error);
throw error;
}
}
// Using the async function
fetchUserData()
.then(data => console.log(data))
.catch(error => console.error(error));
// Or using async/await
async function displayUserData() {
try {
const data = await fetchUserData();
console.log(data);
catch (error) {
} console.error(error);
} }
Practical Examples
1. Sequential vs Parallel Execution
// Sequential execution (one after another)
async function fetchSequential() {
console.time('sequential');
const user = await fetchUserData();
const posts = await fetchUserPosts(user.id);
const comments = await fetchPostComments(posts[0].id);
console.timeEnd('sequential');
return { user, posts, comments };
}
// Parallel execution (all at once)
async function fetchParallel() {
console.time('parallel');
const [user, posts, comments] = await Promise.all([
fetchUserData(),
fetchUserPosts(),
fetchPostComments()
;
])
console.timeEnd('parallel');
return { user, posts, comments };
}
2. Handling Multiple API Calls with Dependencies
async function processUserData(userId) {
try {
// First API call
const user = await fetchUser(userId);
// Multiple parallel calls depending on user data
const [posts, friends, preferences] = await Promise.all([
fetchUserPosts(user.id),
fetchUserFriends(user.id),
fetchUserPreferences(user.id)
;
])
// Process data that depends on previous results
const relevantPosts = await filterPostsByPreferences(posts, preferences);
const friendsActivity = await getFriendsActivity(friends);
return {
,
userposts: relevantPosts,
friends: friendsActivity,
preferences;
}catch (error) {
} console.error('Error processing user data:', error);
throw error;
} }
3. Implementing Retry Logic
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
catch (error) {
} = error;
lastError console.warn(
`Attempt ${attempt} failed. ${
< maxRetries ? 'Retrying...' : 'Max retries reached.'
attempt }`
;
)
if (attempt < maxRetries) {
// Wait longer between each retry
await new Promise(resolve =>
setTimeout(resolve, 1000 * Math.pow(2, attempt - 1))
;
)
}
}
}
throw lastError;
}
4. Loading and Processing Data in Chunks
async function processLargeDataset(datasetId) {
const CHUNK_SIZE = 1000;
let offset = 0;
const results = [];
while (true) {
// Fetch chunk of data
const chunk = await fetchDataChunk(datasetId, offset, CHUNK_SIZE);
if (chunk.length === 0) {
break; // No more data
}
// Process chunk
const processedChunk = await Promise.all(
.map(async item => {
chunkconst enrichedData = await enrichItem(item);
return processItem(enrichedData);
});
)
.push(...processedChunk);
results+= CHUNK_SIZE;
offset
// Optional: Add delay to prevent overwhelming the server
await new Promise(resolve => setTimeout(resolve, 100));
}
return results;
}
5. Implementation of a Rate Limiter
class RateLimiter {
constructor(maxRequests, timeWindow) {
this.maxRequests = maxRequests;
this.timeWindow = timeWindow;
this.requests = [];
}
async acquireToken() {
const now = Date.now();
// Remove expired timestamps
this.requests = this.requests.filter(
=> now - timestamp < this.timeWindow
timestamp ;
)
if (this.requests.length >= this.maxRequests) {
const oldestRequest = this.requests[0];
const waitTime = this.timeWindow - (now - oldestRequest);
await new Promise(resolve => setTimeout(resolve, waitTime));
return this.acquireToken();
}
this.requests.push(now);
return true;
}
async executeRequest(fn) {
await this.acquireToken();
return fn();
}
}
// Usage example
const apiLimiter = new RateLimiter(5, 1000); // 5 requests per second
async function makeApiRequest(url) {
return apiLimiter.executeRequest(async () => {
const response = await fetch(url);
return response.json();
;
}) }
Error Handling Patterns
// Pattern 1: Try-Catch Block
async function handleWithTryCatch() {
try {
const result = await riskyOperation();
return result;
catch (error) {
} // Handle specific error types
if (error instanceof NetworkError) {
// Handle network error
else if (error instanceof ValidationError) {
} // Handle validation error
else {
} // Handle unknown error
}finally {
} // Cleanup code
}
}
// Pattern 2: Higher-Order Function for Error Handling
const withErrorHandling = (fn) => async (...args) => {
try {
return await fn(...args);
catch (error) {
} console.error(`Error in ${fn.name}:`, error);
throw error;
};
}
// Usage
const safeFetch = withErrorHandling(async (url) => {
const response = await fetch(url);
return response.json();
; })
Best Practices and Considerations
- Error Handling
- Always use try-catch blocks with async/await
- Consider creating error handling wrappers for common patterns
- Include proper cleanup in finally blocks
- Performance
- Use Promise.all() for parallel execution when possible
- Be mindful of memory usage with large datasets
- Implement proper error recovery and retry mechanisms
- Code Organization
- Keep async functions focused and single-purpose
- Consider breaking down complex async operations into smaller functions
- Use meaningful variable names for promises and async operations
- Testing
- Test both success and error scenarios
- Mock external dependencies
- Consider timing issues in tests
Common Pitfalls to Avoid
- Forgetting await
// Wrong
const data = fetchData(); // Returns a promise, not the data
// Correct
const data = await fetchData();
- Using await in forEach
// Wrong - forEach doesn't wait for async operations
.forEach(async (item) => {
itemsawait processItem(item);
;
})
// Correct - use for...of or Promise.all with map
for (const item of items) {
await processItem(item);
}
// Or
await Promise.all(items.map(async (item) => {
await processItem(item);
; }))
Async/await provides a clean and intuitive way to handle asynchronous operations in JavaScript. By understanding its patterns and best practices, you can write more maintainable and efficient asynchronous code. The examples provided in this guide demonstrate various real-world applications and common patterns that you can adapt for your own projects.