Learning Objectives
- Understand async functions and await keyword
- Handle errors with try/catch
- Run async operations in parallel with Promise.all
- Master sequential vs parallel patterns
- Avoid common async/await pitfalls
The Problem with Promises
Promises are great, but chaining can get verbose:
fetch('/api/user')
.then(response => response.json())
.then(user => fetch(`/api/posts/${user.id}`))
.then(response => response.json())
.then(posts => {
console.log(posts);
})
.catch(error => console.error(error));
async/await to the Rescue
async function getPosts() {
try {
const response = await fetch('/api/user');
const user = await response.json();
const postsResponse = await fetch(`/api/posts/${user.id}`);
const posts = await postsResponse.json();
console.log(posts);
} catch (error) {
console.error(error);
}
}
Much cleaner and easier to read!
async Functions
The async keyword makes a function return a Promise:
async function greet() {
return 'Hello';
}
// Equivalent to:
function greet() {
return Promise.resolve('Hello');
}
// Usage
greet().then(message => console.log(message)); // "Hello"
// Or with await
const message = await greet();
console.log(message); // "Hello"
await Keyword
await pauses execution until the Promise resolves:
async function fetchData() {
console.log('Fetching...');
const response = await fetch('/api/data');
console.log('Got response');
const data = await response.json();
console.log('Parsed data');
return data;
}
Important:
await only works inside async functions!
// Error!
function getData() {
const data = await fetch('/api/data'); // SyntaxError!
}
// Correct
async function getData() {
const data = await fetch('/api/data'); // Works!
}
Error Handling with try/catch
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('Failed to fetch user:', error);
return null;
}
}
Multiple try/catch Blocks
async function processData() {
let user;
try {
user = await fetchUser(1);
} catch (error) {
console.error('User fetch failed:', error);
return;
}
try {
await saveToDatabase(user);
} catch (error) {
console.error('Database save failed:', error);
// Continue with other operations
}
console.log('Processing complete');
}
Sequential vs Parallel Execution
Sequential (Slow)
async function fetchSequential() {
const user = await fetchUser(1); // Wait 1 second
const posts = await fetchPosts(1); // Then wait 1 second
const comments = await fetchComments(1); // Then wait 1 second
return { user, posts, comments };
}
// Total time: 3 seconds (1s + 1s + 1s)
Parallel (Fast)
async function fetchParallel() {
// Start all requests simultaneously
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
return { user, posts, comments };
}
// Total time: 1 second (all run simultaneously)
Promise.all for Parallel Execution
async function fetchMultipleUsers(ids) {
const promises = ids.map(id => fetchUser(id));
const users = await Promise.all(promises);
return users;
}
// Fetch users 1, 2, 3 in parallel
const users = await fetchMultipleUsers([1, 2, 3]);
console.log(users); // [user1, user2, user3]
Handle Individual Failures
async function fetchWithFallback(ids) {
const promises = ids.map(id =>
fetchUser(id).catch(error => ({
error: error.message,
id
}))
);
const results = await Promise.all(promises);
const successful = results.filter(r => !r.error);
const failed = results.filter(r => r.error);
return { successful, failed };
}
Promise.allSettled
Get all results, even if some fail:
async function fetchAll(ids) {
const promises = ids.map(id => fetchUser(id));
const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`User ${ids[index]}:`, result.value);
} else {
console.error(`User ${ids[index]} failed:`, result.reason);
}
});
return results;
}
Promise.race
Return first completed Promise:
async function fetchWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
);
try {
const response = await Promise.race([fetchPromise, timeoutPromise]);
return await response.json();
} catch (error) {
if (error.message === 'Timeout') {
console.error('Request timed out');
}
throw error;
}
}
Real-World Examples
API Request with Retry
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (response.ok) {
return await response.json();
}
throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (i === retries - 1) throw error;
const delay = 1000 * (i + 1);
console.log(`Retry ${i + 1} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
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);
console.log(`Processing batch ${i / batchSize + 1}`);
const batchResults = await Promise.all(
batch.map(item => processItem(item))
);
results.push(...batchResults);
}
return results;
}
// Process 100 items in batches of 5
const items = Array.from({ length: 100 }, (_, i) => i);
const results = await processBatch(items, 5);
Dependent Async Operations
async function createUserWithProfile(userData) {
// These must run sequentially (each depends on previous)
const user = await createUser(userData);
console.log('User created:', user.id);
const profile = await createProfile(user.id, userData.profile);
console.log('Profile created');
const avatar = await uploadAvatar(user.id, userData.avatar);
console.log('Avatar uploaded');
return { user, profile, avatar };
}
Parallel Independent Operations
async function loadDashboard(userId) {
// These can run in parallel (independent)
const [user, stats, notifications, recentActivity] = await Promise.all([
fetchUser(userId),
fetchUserStats(userId),
fetchNotifications(userId),
fetchRecentActivity(userId)
]);
return { user, stats, notifications, recentActivity };
}
Common Pitfalls
Pitfall 1: Forgetting await
// Wrong - returns Promise, not data
async function getData() {
const data = fetch('/api/data'); // Missing await!
console.log(data); // Promise, not data
return data;
}
// Right
async function getData() {
const data = await fetch('/api/data');
return data;
}
Pitfall 2: Sequential when parallel is better
// Slow - sequential (3 seconds)
async function fetchData() {
const users = await fetchUsers(); // 1s
const posts = await fetchPosts(); // 1s
const comments = await fetchComments(); // 1s
return { users, posts, comments };
}
// Fast - parallel (1 second)
async function fetchData() {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return { users, posts, comments };
}
Pitfall 3: Not handling errors
// Bad - unhandled errors crash the app
async function fetchData() {
const data = await fetch('/api/data');
return data;
}
// Good - error handling
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);
return null;
}
}
Pitfall 4: Using await in loops unnecessarily
// Slow - sequential processing
async function processUsers(users) {
const results = [];
for (const user of users) {
const result = await processUser(user); // One at a time
results.push(result);
}
return results;
}
// Fast - parallel processing
async function processUsers(users) {
return await Promise.all(
users.map(user => processUser(user))
);
}
Best Practices
1. Always handle errors
async function safeOperation() {
try {
return await riskyOperation();
} catch (error) {
console.error('Operation failed:', error);
return defaultValue;
}
}
2. Use Promise.all for independent operations
// Good - parallel
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
3. Avoid mixing async/await with .then()
// Bad - mixing styles
async function getData() {
return await fetch('/api/data')
.then(r => r.json());
}
// Good - consistent style
async function getData() {
const response = await fetch('/api/data');
return await response.json();
}
4. Return early on errors
async function processUser(id) {
const user = await fetchUser(id);
if (!user) return null;
const processed = await processData(user);
if (!processed) return null;
return processed;
}
5. Use Promise.allSettled for error-tolerant parallel operations
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(2),
fetchUser(3)
]);
// All results available, even if some failed
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log(result.value);
}
});
Key Takeaways
- async functions always return Promises
- await pauses execution until Promise resolves
- Use try/catch for error handling
- Promise.all for parallel execution of independent operations
- Promise.allSettled to handle all results (success or failure)
- Promise.race for timeout patterns and racing operations
- Avoid sequential execution when parallel is possible
- Always handle errors properly with try/catch
- Don't forget await keyword
- Modern standard for async JavaScript