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