Module 18: Async/Await
Async/await is syntactic sugar built on top of Promises, making asynchronous code look and behave like synchronous code. It's the modern standard for async JavaScript.
1. Understanding Async/Await
1.1 What is async/await?
// Promise-based code
function fetchUserPromise(id) {
return fetchUser(id)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => console.log(details))
.catch(error => console.error(error));
}
// Async/await (cleaner and more readable)
async function fetchUserAsync(id) {
try {
const user = await fetchUser(id);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
console.log(details);
} catch (error) {
console.error(error);
}
}
Under the Hood
async/await is syntactic sugar over Promises. An async function always returns a Promise, and await pauses execution until the Promise settles.
2. Async Functions
2.1 Declaring Async Functions
// Function declaration
async function fetchData() {
return 'data';
}
// Function expression
const fetchData = async function() {
return 'data';
};
// Arrow function
const fetchData = async () => {
return 'data';
};
// Method in object
const api = {
async getData() {
return 'data';
}
};
// Method in class
class API {
async getData() {
return 'data';
}
}
2.2 Return Values
// Always returns a Promise
async function getValue() {
return 42;
}
getValue().then(value => console.log(value)); // 42
// Equivalent to:
function getValue() {
return Promise.resolve(42);
}
// Returning a Promise
async function getUser() {
return fetchUser(1); // Returns the Promise
}
getUser().then(user => console.log(user));
3. Await Operator
3.1 Basic Usage
async function fetchUserData() {
// Wait for Promise to resolve
const user = await fetchUser(1);
console.log('User:', user);
// Continue after Promise resolves
const orders = await getOrders(user.id);
console.log('Orders:', orders);
return { user, orders };
}
fetchUserData();
3.2 await Only in Async Functions
// ❌ Error: await outside async function
function regularFunction() {
const data = await fetchData(); // SyntaxError
}
// ✅ Correct
async function asyncFunction() {
const data = await fetchData(); // Works
}
// ✅ IIFE (Immediately Invoked Function Expression)
(async () => {
const data = await fetchData();
console.log(data);
})();
// ✅ Top-level await (ES2022+, in modules)
const data = await fetchData();
3.3 Multiple Await Calls
async function processData() {
// Sequential (waits for each)
const user = await fetchUser(1); // Wait 1 second
const posts = await fetchPosts(1); // Wait 1 second
const comments = await fetchComments(1); // Wait 1 second
// Total: ~3 seconds
return { user, posts, comments };
}
4. Error Handling
4.1 try...catch
async function fetchUser(id) {
try {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('Error fetching user:', error.message);
throw error; // Re-throw or handle
}
}
// Usage
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error('Failed:', error));
4.2 Multiple try...catch Blocks
async function processUser(id) {
let user;
try {
user = await fetchUser(id);
} catch (error) {
console.error('Failed to fetch user:', error);
return null;
}
try {
const orders = await getOrders(user.id);
return { user, orders };
} catch (error) {
console.error('Failed to fetch orders:', error);
return { user, orders: [] };
}
}
4.3 finally Block
async function fetchData() {
let isLoading = true;
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Error:', error);
throw error;
} finally {
isLoading = false;
console.log('Request completed');
}
}
4.4 Handling Promise Rejections
async function example() {
// Method 1: try...catch
try {
const result = await riskyOperation();
return result;
} catch (error) {
console.error('Error:', error);
}
// Method 2: .catch() on the promise
const result = await riskyOperation().catch(error => {
console.error('Error:', error);
return null; // Fallback value
});
return result;
}
5. Parallel Execution
5.1 Sequential vs Parallel
// ❌ Sequential (slow)
async function fetchSequential() {
const user = await fetchUser(1); // 1 second
const posts = await fetchPosts(1); // 1 second
const comments = await fetchComments(1); // 1 second
// Total: ~3 seconds
return { user, posts, comments };
}
// ✅ Parallel (fast)
async function fetchParallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
// Total: ~1 second (all run in parallel)
return { user, posts, comments };
}
5.2 Promise.all with async/await
async function fetchAllUsers(ids) {
try {
const users = await Promise.all(
ids.map(id => fetchUser(id))
);
return users;
} catch (error) {
console.error('One of the requests failed:', error);
throw error;
}
}
// Usage
const userIds = [1, 2, 3, 4, 5];
const users = await fetchAllUsers(userIds);
console.log('Users:', users);
5.3 Promise.allSettled
async function fetchAllUsersSettled(ids) {
const results = await Promise.allSettled(
ids.map(id => fetchUser(id))
);
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
console.log('Successful:', successful.length);
console.log('Failed:', failed.length);
return successful;
}
5.4 Promise.race
async function fetchWithTimeout(url, timeout = 5000) {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
);
try {
const response = await Promise.race([
fetch(url),
timeoutPromise
]);
return await response.json();
} catch (error) {
console.error('Request failed:', error.message);
throw error;
}
}
// Usage
const data = await fetchWithTimeout('https://api.example.com/data', 3000);
6. Common Patterns
6.1 Retry Logic
async function retry(fn, maxAttempts = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) {
throw error;
}
console.log(`Attempt ${attempt} failed, retrying...`);
await sleep(delay);
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Usage
const data = await retry(() => fetchData(), 3, 1000);
6.2 Sequential Processing
async function processItemsSequentially(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
// Or with reduce
async function processItemsSequentially(items) {
return await items.reduce(async (promiseChain, item) => {
const results = await promiseChain;
const result = await processItem(item);
return [...results, result];
}, Promise.resolve([]));
}
6.3 Batch Processing
async function processBatch(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;
}
6.4 Rate Limiting
class RateLimiter {
constructor(maxConcurrent, minDelay = 0) {
this.maxConcurrent = maxConcurrent;
this.minDelay = minDelay;
this.running = 0;
this.queue = [];
}
async execute(fn) {
while (this.running >= this.maxConcurrent) {
await new Promise(resolve => this.queue.push(resolve));
}
this.running++;
try {
return await fn();
} finally {
this.running--;
if (this.minDelay > 0) {
await sleep(this.minDelay);
}
const resolve = this.queue.shift();
if (resolve) resolve();
}
}
}
// Usage
const limiter = new RateLimiter(3, 100); // Max 3 concurrent, 100ms between
const promises = urls.map(url =>
limiter.execute(() => fetch(url))
);
const responses = await Promise.all(promises);
7. Practical Examples
7.1 API Client
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.token = null;
}
async setToken(token) {
this.token = token;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
try {
const response = await fetch(url, {
...options,
headers
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
async get(endpoint) {
return await this.request(endpoint, { method: 'GET' });
}
async post(endpoint, data) {
return await this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
async put(endpoint, data) {
return await this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
async delete(endpoint) {
return await this.request(endpoint, { method: 'DELETE' });
}
}
// Usage
const api = new APIClient('https://api.example.com');
async function main() {
try {
const users = await api.get('/users');
console.log('Users:', users);
const newUser = await api.post('/users', {
name: 'John Doe',
email: 'john@example.com'
});
console.log('Created user:', newUser);
} catch (error) {
console.error('Error:', error);
}
}
7.2 Data Fetching with Caching
class DataCache {
constructor(ttl = 60000) {
this.cache = new Map();
this.ttl = ttl;
}
async get(key, fetcher) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
const data = await fetcher();
this.cache.set(key, {
data,
timestamp: Date.now()
});
return data;
}
clear() {
this.cache.clear();
}
}
// Usage
const cache = new DataCache(60000); // 1 minute TTL
async function getUser(id) {
return await cache.get(`user:${id}`, async () => {
const response = await fetch(`https://api.example.com/users/${id}`);
return await response.json();
});
}
// First call: fetches from API
const user1 = await getUser(1);
// Second call within 1 minute: returns cached data
const user2 = await getUser(1);
7.3 Parallel Data Loading
async function loadUserDashboard(userId) {
try {
// Start all requests in parallel
const [user, posts, followers, notifications] = await Promise.all([
fetchUser(userId),
fetchUserPosts(userId),
fetchFollowers(userId),
fetchNotifications(userId)
]);
// Process data
const dashboard = {
user,
stats: {
posts: posts.length,
followers: followers.length,
unreadNotifications: notifications.filter(n => !n.read).length
},
recentPosts: posts.slice(0, 5),
recentFollowers: followers.slice(0, 10)
};
return dashboard;
} catch (error) {
console.error('Failed to load dashboard:', error);
throw error;
}
}
// Usage
const dashboard = await loadUserDashboard(123);
console.log('Dashboard:', dashboard);
7.4 Form Submission with Validation
class FormHandler {
constructor(formId) {
this.form = document.getElementById(formId);
this.errors = {};
}
async validate(data) {
this.errors = {};
if (!data.email || !data.email.includes('@')) {
this.errors.email = 'Invalid email address';
}
if (!data.password || data.password.length < 8) {
this.errors.password = 'Password must be at least 8 characters';
}
// Async validation: check if email exists
if (data.email && !this.errors.email) {
try {
const response = await fetch(`/api/check-email?email=${data.email}`);
const { exists } = await response.json();
if (exists) {
this.errors.email = 'Email already registered';
}
} catch (error) {
console.error('Validation error:', error);
}
}
return Object.keys(this.errors).length === 0;
}
async submit(data) {
try {
const isValid = await this.validate(data);
if (!isValid) {
this.showErrors();
return null;
}
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error('Registration failed');
}
return await response.json();
} catch (error) {
console.error('Submission error:', error);
throw error;
}
}
showErrors() {
for (const [field, message] of Object.entries(this.errors)) {
console.error(`${field}: ${message}`);
}
}
}
8. Best Practices
8.1 Always Handle Errors
// ❌ Bad: Unhandled promise rejection
async function fetchData() {
const data = await fetch('/api/data');
return data.json();
}
// ✅ Good: Handle errors
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Fetch failed');
return await response.json();
} catch (error) {
console.error('Error:', error);
throw error;
}
}
8.2 Avoid Sequential When Parallel is Possible
// ❌ Bad: Unnecessary sequential execution
async function fetchData() {
const users = await fetchUsers();
const posts = await fetchPosts();
const comments = await fetchComments();
return { users, posts, comments };
}
// ✅ Good: Parallel execution
async function fetchData() {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return { users, posts, comments };
}
8.3 Use async/await with Array Methods
// ❌ Bad: forEach doesn't wait
async function processItems(items) {
items.forEach(async (item) => {
await processItem(item); // Doesn't wait!
});
console.log('Done'); // Logs before items are processed
}
// ✅ Good: Use for...of for sequential
async function processItems(items) {
for (const item of items) {
await processItem(item);
}
console.log('Done'); // Logs after all items processed
}
// ✅ Good: Use Promise.all for parallel
async function processItems(items) {
await Promise.all(items.map(item => processItem(item)));
console.log('Done');
}
8.4 Top-Level Await (ES2022+)
// In module scope (ES modules)
const data = await fetchData();
const user = await getUser(1);
console.log('Data:', data);
console.log('User:', user);
// Or wrap in IIFE for older environments
(async () => {
const data = await fetchData();
console.log('Data:', data);
})();
9. Common Mistakes
9.1 Forgetting await
// ❌ Bad
async function getData() {
const data = fetch('/api/data'); // Missing await!
console.log(data); // Promise, not the data
return data;
}
// ✅ Good
async function getData() {
const data = await fetch('/api/data');
console.log(data);
return data;
}
9.2 Using await in Loops Unnecessarily
// ❌ Bad: Sequential (slow)
for (let i = 0; i < ids.length; i++) {
const user = await fetchUser(ids[i]);
users.push(user);
}
// ✅ Good: Parallel (fast)
const users = await Promise.all(
ids.map(id => fetchUser(id))
);
9.3 Not Returning from Async Functions
// ❌ Bad
async function processData() {
const data = await fetchData();
processResult(data); // Forgotten await
}
// ✅ Good
async function processData() {
const data = await fetchData();
return await processResult(data);
}
Summary
In this module, you learned:
- ✅ What async/await is and how it works
- ✅ Declaring and using async functions
- ✅ Error handling with try...catch
- ✅ Parallel execution with Promise.all
- ✅ Common patterns: retry, batching, rate limiting
- ✅ Practical examples: API clients, caching, form handling
- ✅ Best practices and common mistakes
- ✅ Modern features like top-level await
Next Steps
In Module 19, you'll learn about Modules (Import/Export) for organizing code into reusable components.
Practice Exercises
- Convert Promise-based code to async/await
- Build an async API client with retry logic
- Implement parallel data fetching with error handling
- Create a batch processor for large datasets
- Build a rate-limited web scraper
- Implement an async cache with expiration
- Create a form with async validation
- Build a concurrent task queue