A guide to async/await in JavaScript with examples
Mastering async
/await
in JavaScript: A Practical Guide
Asynchronous programming is essential for modern JavaScript development. The async/await
syntax introduced in ES2017 has revolutionized how we handle asynchronous operations, making the code more readable and easier to maintain. In this post, we'll explore how to use async/await
effectively through various scenarios with code examples.
Understanding async
and await
async
functions enable you to write asynchronous code that looks and behaves like synchronous code. The await keyword pauses the execution of an async
function until a promise is resolved, making it easier to work with promises.
Basic Example
Here's a simple example of how to use async
and await
:
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
}
fetchData();
In this example, fetchData
is an async function that fetches data from an API. The await
keyword pauses the function until the fetch
promise resolves.
Error Handling
Handling errors in async
functions is straightforward with try/catch
:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch error:', error);
}
}
fetchData();
In this example, if the fetch operation fails or the response is not OK, the error is caught and logged.
Multiple await
Statements
You can wait for multiple promises using await
:
async function fetchMultipleData() {
const [response1, response2] = await Promise.all([
fetch('https://api.example.com/data1'),
fetch('https://api.example.com/data2')
]);
const data1 = await response1.json();
const data2 = await response2.json();
console.log(data1, data2);
}
fetchMultipleData();
Here, Promise.all
runs multiple promises in parallel, and await
waits for all of them to resolve.
Sequential await
Statements
For sequential operations, use multiple await statements one after another:
async function fetchSequentialData() {
const response1 = await fetch('https://api.example.com/data1');
const data1 = await response1.json();
const response2 = await fetch('https://api.example.com/data2');
const data2 = await response2.json();
console.log(data1, data2);
}
fetchSequentialData();
This approach ensures that the second fetch operation does not start until the first one completes.
Using async/await
in Loops
You can use async
/await
in loops, but be mindful of performance implications:
async function fetchInLoop(urls) {
for (const url of urls) {
const response = await fetch(url);
const data = await response.json();
console.log(data);
}
}
const urls = ['https://api.example.com/data1', 'https://api.example.com/data2'];
fetchInLoop(urls);
This fetches data sequentially. For parallel fetching, use Promise.all
:
async function fetchInParallel(urls) {
const promises = urls.map(url => fetch(url));
const responses = await Promise.all(promises);
const dataPromises = responses.map(response => response.json());
const data = await Promise.all(dataPromises);
console.log(data);
}
fetchInParallel(urls);
Real-World Examples
1. Retrying Failed Requests
Sometimes API calls fail and need to be retried. Here's a practical implementation of a retry mechanism:
async function fetchWithRetry(url, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error; // Last attempt failed
console.log(`Attempt ${i + 1} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
fetchWithRetry('https://api.example.com/data')
.then(data => console.log('Success:', data))
.catch(error => console.error('Final error:', error));
This function attempts to fetch data up to three times, with a delay between attempts. It's useful for handling temporary network issues or rate limiting.
2. Loading Data with Progress Updates
Here's how to handle multiple async operations while showing progress:
async function loadDataWithProgress(items) {
const total = items.length;
const results = [];
for (let i = 0; i < total; i++) {
try {
const result = await fetch(items[i]);
const data = await result.json();
results.push(data);
// Calculate and report progress
const progress = ((i + 1) / total * 100).toFixed(1);
console.log(`Progress: ${progress}%`);
} catch (error) {
console.error(`Failed to load item ${i + 1}:`, error);
}
}
return results;
}
// Usage
const urls = [
'https://api.example.com/user/1',
'https://api.example.com/user/2',
'https://api.example.com/user/3'
];
loadDataWithProgress(urls);
This example shows how to track progress when loading multiple items sequentially.
3. Implementing a Rate Limiter
Here's a practical example of rate limiting API calls:
class RateLimiter {
constructor(maxRequests, timeWindow) {
this.queue = [];
this.maxRequests = maxRequests;
this.timeWindow = timeWindow;
}
async execute(fn) {
// Remove old timestamps
const now = Date.now();
this.queue = this.queue.filter(timestamp =>
now - timestamp < this.timeWindow
);
// Wait if queue is full
if (this.queue.length >= this.maxRequests) {
const oldestTimestamp = this.queue[0];
const waitTime = oldestTimestamp + this.timeWindow - now;
await new Promise(resolve => setTimeout(resolve, waitTime));
}
// Add new timestamp and execute function
this.queue.push(now);
return fn();
}
}
// Usage example
const limiter = new RateLimiter(2, 1000); // 2 requests per second
async function fetchWithRateLimit(urls) {
const results = [];
for (const url of urls) {
const result = await limiter.execute(async () => {
const response = await fetch(url);
return response.json();
});
results.push(result);
}
return results;
}
This implementation ensures that API calls are spaced out to prevent overwhelming the server or hitting rate limits.
4. Async Event Handler with Debouncing
Here's how to implement a debounced async search function:
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
return new Promise(resolve => {
const later = async () => {
clearTimeout(timeout);
resolve(await func(...args));
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
});
};
}
const searchAPI = async (query) => {
const response = await fetch(`https://api.example.com/search?q=${query}`);
return response.json();
};
const debouncedSearch = debounce(searchAPI, 300);
// Usage in an event listener
async function handleSearch(event) {
try {
const results = await debouncedSearch(event.target.value);
console.log('Search results:', results);
} catch (error) {
console.error('Search failed:', error);
}
}
This example shows how to combine async
/await
with debouncing, which is useful for search inputs or other frequent user interactions.
Conclusion
async
/await
makes asynchronous JavaScript code cleaner and more readable. Whether you're dealing with API calls, handling multiple operations, or managing errors, this syntax can significantly simplify your code.
Happy coding!