⏳ Async/Await

ES2017 Syntactic Sugar for Promises

What is Async/Await?

Async/await is syntactic sugar over Promises, making asynchronous code look and behave more like synchronous code. It provides a cleaner way to work with Promises.

πŸ”€ async Keyword

// async function always returns a promise
async function getData() {
    return 'Hello';
}

getData().then(value => console.log(value));  // 'Hello'

// Same as
function getData() {
    return Promise.resolve('Hello');
}

// Return promise directly
async function fetchUser() {
    return fetch('/api/user').then(r => r.json());
}

// Throw error = rejected promise
async function failingFunction() {
    throw new Error('Oops');
}

failingFunction().catch(error => {
    console.error(error.message);  // 'Oops'
});

// async arrow function
const getData = async () => {
    return 'Hello';
};

// async method
class API {
    async getUser(id) {
        return fetch(`/api/users/${id}`).then(r => r.json());
    }
}

⏸️ await Keyword

// await pauses execution until promise resolves
async function getData() {
    let response = await fetch('/api/data');
    let data = await response.json();
    return data;
}

// Without await (promise chain)
function getData() {
    return fetch('/api/data')
        .then(response => response.json())
        .then(data => data);
}

// Can only use await inside async function
async function example() {
    let result = await somePromise();  // OK
}

// await somePromise();  // SyntaxError: outside async function

// Top-level await (ES2022, in modules only)
// script.js (type="module")
let data = await fetch('/api/data').then(r => r.json());
console.log(data);

// await unwraps promise
async function example() {
    let promise = Promise.resolve(42);
    let value = await promise;
    console.log(value);  // 42 (not a promise)
    console.log(typeof value);  // 'number'
}

πŸ›‘οΈ Error Handling

try-catch

// Catch errors with try-catch
async function fetchData() {
    try {
        let response = await fetch('/api/data');
        let data = await response.json();
        return data;
    } catch (error) {
        console.error('Failed to fetch:', error);
        return null;
    }
}

// Multiple operations
async function loadDashboard() {
    try {
        let user = await fetchUser();
        let posts = await fetchPosts(user.id);
        let stats = await fetchStats();
        
        return { user, posts, stats };
    } catch (error) {
        console.error('Dashboard load failed:', error);
        throw error;  // Re-throw if needed
    }
}

// Finally block
async function processData() {
    let file;
    try {
        file = await openFile('data.txt');
        let data = await readFile(file);
        return processedData(data);
    } catch (error) {
        console.error('Processing failed:', error);
        return null;
    } finally {
        if (file) {
            await closeFile(file);  // Cleanup
        }
    }
}

catch() on await

// Catch specific operation
async function example() {
    let data = await fetchData().catch(error => {
        console.error('Fetch failed:', error);
        return defaultData;  // Fallback
    });
    
    console.log(data);  // Either fetched or default
}

// Mix try-catch and .catch()
async function hybrid() {
    try {
        let user = await fetchUser();
        
        // Handle this error specifically
        let posts = await fetchPosts(user.id).catch(error => {
            console.warn('No posts:', error);
            return [];  // Empty array fallback
        });
        
        return { user, posts };
    } catch (error) {
        console.error('Critical error:', error);
    }
}

Error Propagation

// Errors bubble up
async function level3() {
    throw new Error('Level 3 error');
}

async function level2() {
    await level3();  // Error propagates
}

async function level1() {
    try {
        await level2();
    } catch (error) {
        console.error('Caught at level 1:', error.message);
    }
}

level1();  // 'Caught at level 1: Level 3 error'

// Without try-catch, becomes unhandled rejection
async function unhandled() {
    await Promise.reject('Error');  // Unhandled
}

unhandled();  // UnhandledPromiseRejectionWarning

// Always handle at some level
async function handled() {
    try {
        await unhandled();
    } catch (error) {
        console.error('Handled:', error);
    }
}

πŸ”„ Patterns and Techniques

Sequential Operations

// Operations run one after another
async function sequential() {
    let user = await fetchUser();      // Wait
    let posts = await fetchPosts();    // Then wait
    let comments = await fetchComments();  // Then wait
    
    return { user, posts, comments };
}

// Total time = sum of all operations

// Process array sequentially
async function processSequentially(items) {
    let results = [];
    
    for (let item of items) {
        let result = await processItem(item);
        results.push(result);
    }
    
    return results;
}

// for-of with await
async function example() {
    let urls = ['/api/1', '/api/2', '/api/3'];
    
    for (let url of urls) {
        let response = await fetch(url);
        let data = await response.json();
        console.log(data);
    }
}

Parallel Operations

// Run operations in parallel (faster)
async function parallel() {
    // Start all promises
    let userPromise = fetchUser();
    let postsPromise = fetchPosts();
    let commentsPromise = fetchComments();
    
    // Wait for all to complete
    let user = await userPromise;
    let posts = await postsPromise;
    let comments = await commentsPromise;
    
    return { user, posts, comments };
}

// Or use Promise.all
async function parallelAll() {
    let [user, posts, comments] = await Promise.all([
        fetchUser(),
        fetchPosts(),
        fetchComments()
    ]);
    
    return { user, posts, comments };
}

// Process array in parallel
async function processParallel(items) {
    let promises = items.map(item => processItem(item));
    let results = await Promise.all(promises);
    return results;
}

// Real example
async function loadDashboard() {
    let [users, posts, stats] = await Promise.all([
        fetch('/api/users').then(r => r.json()),
        fetch('/api/posts').then(r => r.json()),
        fetch('/api/stats').then(r => r.json())
    ]);
    
    return { users, posts, stats };
}

Conditional Operations

// Conditional await
async function example(shouldFetch) {
    if (shouldFetch) {
        let data = await fetchData();
        return data;
    }
    return cachedData;
}

// Early return
async function getUser(id) {
    if (!id) {
        return null;
    }
    
    let user = await fetchUser(id);
    
    if (!user.isActive) {
        return null;
    }
    
    let profile = await fetchProfile(user.id);
    return { ...user, profile };
}

// Retry logic
async function fetchWithRetry(url, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            let response = await fetch(url);
            if (response.ok) {
                return await response.json();
            }
        } catch (error) {
            if (i === retries - 1) throw error;
            await new Promise(r => setTimeout(r, 1000 * (i + 1)));
        }
    }
}

Timeout Pattern

// Add timeout to async operation
async function withTimeout(promise, ms) {
    let timeout = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Timeout')), ms);
    });
    
    return Promise.race([promise, timeout]);
}

// Usage
async function example() {
    try {
        let data = await withTimeout(fetchData(), 5000);
        console.log(data);
    } catch (error) {
        if (error.message === 'Timeout') {
            console.error('Request timed out');
        } else {
            console.error('Request failed:', error);
        }
    }
}

// AbortController for fetch timeout
async function fetchWithTimeout(url, ms) {
    let controller = new AbortController();
    let timeout = setTimeout(() => controller.abort(), ms);
    
    try {
        let response = await fetch(url, { signal: controller.signal });
        clearTimeout(timeout);
        return await response.json();
    } catch (error) {
        if (error.name === 'AbortError') {
            throw new Error('Request timed out');
        }
        throw error;
    }
}

πŸ’‘ Practical Examples

API Client

class APIClient {
    constructor(baseURL) {
        this.baseURL = baseURL;
    }
    
    async get(endpoint) {
        let response = await fetch(`${this.baseURL}${endpoint}`);
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        return response.json();
    }
    
    async post(endpoint, data) {
        let response = await fetch(`${this.baseURL}${endpoint}`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        return response.json();
    }
    
    async delete(endpoint) {
        let response = await fetch(`${this.baseURL}${endpoint}`, {
            method: 'DELETE'
        });
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        return response.json();
    }
}

// Usage
let api = new APIClient('https://api.example.com');

async function loadUsers() {
    try {
        let users = await api.get('/users');
        console.log(users);
    } catch (error) {
        console.error('Failed to load users:', error);
    }
}

Form Submission

async function handleSubmit(event) {
    event.preventDefault();
    
    let form = event.target;
    let submitBtn = form.querySelector('button[type="submit"]');
    let errorDiv = form.querySelector('.error');
    
    // Disable button
    submitBtn.disabled = true;
    submitBtn.textContent = 'Submitting...';
    errorDiv.textContent = '';
    
    try {
        let formData = new FormData(form);
        let data = Object.fromEntries(formData);
        
        let response = await fetch('/api/submit', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });
        
        if (!response.ok) {
            let error = await response.json();
            throw new Error(error.message);
        }
        
        let result = await response.json();
        console.log('Success:', result);
        
        form.reset();
        showNotification('Submitted successfully!');
        
    } catch (error) {
        errorDiv.textContent = error.message;
        console.error('Submission failed:', error);
    } finally {
        submitBtn.disabled = false;
        submitBtn.textContent = 'Submit';
    }
}

document.querySelector('form').addEventListener('submit', handleSubmit);

Data Loading with Cache

class DataLoader {
    constructor() {
        this.cache = new Map();
    }
    
    async load(key, fetcher, ttl = 60000) {
        // Check cache
        if (this.cache.has(key)) {
            let { data, timestamp } = this.cache.get(key);
            if (Date.now() - timestamp < ttl) {
                return data;
            }
        }
        
        // Fetch new data
        let data = await fetcher();
        
        // Store in cache
        this.cache.set(key, {
            data,
            timestamp: Date.now()
        });
        
        return data;
    }
    
    clear(key) {
        if (key) {
            this.cache.delete(key);
        } else {
            this.cache.clear();
        }
    }
}

// Usage
let loader = new DataLoader();

async function getUser(id) {
    return loader.load(
        `user:${id}`,
        () => fetch(`/api/users/${id}`).then(r => r.json()),
        30000  // 30 second TTL
    );
}

// First call fetches
let user1 = await getUser(1);

// Second call uses cache
let user2 = await getUser(1);

Batch Operations

async function processBatch(items, batchSize = 10) {
    let results = [];
    
    for (let i = 0; i < items.length; i += batchSize) {
        let batch = items.slice(i, i + batchSize);
        
        console.log(`Processing batch ${i / batchSize + 1}...`);
        
        let batchResults = await Promise.all(
            batch.map(item => processItem(item))
        );
        
        results.push(...batchResults);
    }
    
    return results;
}

// Usage
let items = Array.from({ length: 100 }, (_, i) => i);

async function processAll() {
    try {
        let results = await processBatch(items, 10);
        console.log(`Processed ${results.length} items`);
    } catch (error) {
        console.error('Batch processing failed:', error);
    }
}

processAll();

Concurrent Limit

async function mapWithLimit(items, limit, fn) {
    let results = [];
    let executing = [];
    
    for (let [index, item] of items.entries()) {
        let promise = Promise.resolve().then(() => fn(item, index));
        results.push(promise);
        
        if (limit <= items.length) {
            let e = promise.then(() => {
                executing.splice(executing.indexOf(e), 1);
            });
            executing.push(e);
            
            if (executing.length >= limit) {
                await Promise.race(executing);
            }
        }
    }
    
    return Promise.all(results);
}

// Usage: max 3 concurrent requests
let urls = Array.from({ length: 20 }, (_, i) => `/api/item/${i}`);

async function loadAll() {
    let results = await mapWithLimit(urls, 3, async (url) => {
        let response = await fetch(url);
        return response.json();
    });
    
    console.log('Loaded:', results.length);
}

loadAll();

Polling

async function poll(fn, interval = 1000, maxAttempts = 10) {
    for (let i = 0; i < maxAttempts; i++) {
        try {
            let result = await fn();
            if (result) {
                return result;
            }
        } catch (error) {
            console.warn(`Attempt ${i + 1} failed:`, error);
        }
        
        if (i < maxAttempts - 1) {
            await new Promise(resolve => setTimeout(resolve, interval));
        }
    }
    
    throw new Error('Max attempts reached');
}

// Usage: wait for job completion
async function waitForJob(jobId) {
    return poll(
        async () => {
            let response = await fetch(`/api/jobs/${jobId}`);
            let job = await response.json();
            
            if (job.status === 'completed') {
                return job;
            } else if (job.status === 'failed') {
                throw new Error('Job failed');
            }
            
            return null;  // Keep polling
        },
        2000,  // Check every 2 seconds
        30     // Max 30 attempts (1 minute)
    );
}

try {
    let job = await waitForJob('job-123');
    console.log('Job completed:', job);
} catch (error) {
    console.error('Job polling failed:', error);
}

Async Iteration

// Async generators
async function* fetchPages(url) {
    let page = 1;
    let hasMore = true;
    
    while (hasMore) {
        let response = await fetch(`${url}?page=${page}`);
        let data = await response.json();
        
        yield data.items;
        
        hasMore = data.hasMore;
        page++;
    }
}

// Usage with for-await-of
async function loadAllPages() {
    for await (let items of fetchPages('/api/items')) {
        console.log('Loaded page:', items.length);
        processItems(items);
    }
}

// Async iterable class
class AsyncQueue {
    constructor() {
        this.queue = [];
        this.waiting = [];
    }
    
    push(item) {
        if (this.waiting.length > 0) {
            let resolve = this.waiting.shift();
            resolve(item);
        } else {
            this.queue.push(item);
        }
    }
    
    async next() {
        if (this.queue.length > 0) {
            return this.queue.shift();
        }
        
        return new Promise(resolve => {
            this.waiting.push(resolve);
        });
    }
    
    async *[Symbol.asyncIterator]() {
        while (true) {
            yield await this.next();
        }
    }
}

// Usage
let queue = new AsyncQueue();

async function consumer() {
    for await (let item of queue) {
        console.log('Processing:', item);
        if (item === 'END') break;
    }
}

consumer();
queue.push('Item 1');
queue.push('Item 2');
queue.push('END');

⚠️ Common Mistakes

// Forgetting await
async function bad() {
    let data = fetchData();  // Returns promise, not data!
    console.log(data);  // Promise {  }
}

async function good() {
    let data = await fetchData();
    console.log(data);  // Actual data
}

// Sequential when should be parallel
async function slow() {
    let user = await fetchUser();      // Wait
    let posts = await fetchPosts();    // Wait
    let stats = await fetchStats();    // Wait
}

async function fast() {
    let [user, posts, stats] = await Promise.all([
        fetchUser(),
        fetchPosts(),
        fetchStats()
    ]);
}

// Forgetting error handling
async function risky() {
    let data = await fetchData();  // Might throw
    return data;
}

risky();  // Unhandled promise rejection!

async function safe() {
    try {
        let data = await fetchData();
        return data;
    } catch (error) {
        console.error(error);
    }
}

// Using await in loop when parallel is better
async function inefficient(ids) {
    let users = [];
    for (let id of ids) {
        let user = await fetchUser(id);  // One at a time
        users.push(user);
    }
    return users;
}

async function efficient(ids) {
    return Promise.all(ids.map(id => fetchUser(id)));
}

// Async forEach doesn't work as expected
async function broken(items) {
    items.forEach(async item => {
        await processItem(item);  // Doesn't wait!
    });
    console.log('Done?');  // Runs immediately
}

async function fixed(items) {
    for (let item of items) {
        await processItem(item);  // Properly waits
    }
    console.log('Done!');  // Runs after all
}

🎯 Key Takeaways