Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
20 minLesson 24 of 35
Asynchronous JavaScript

async/await: Clean Async Code

async/await: Clean Async Code

async/await transformed how JavaScript developers write asynchronous code. Instead of Promise chains, you write code that looks and reads like synchronous code — while still being fully non-blocking.

The Basics

// An async function always returns a Promise
async function fetchUser(id) {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    return user;  // Automatically wrapped in Promise.resolve()
}

// await pauses execution until the Promise resolves
// But it only pauses the async function — the rest of the JS thread keeps running
// Using the function
fetchUser(1).then(user => console.log(user));

// Or with await in another async function
async function main() {
    const user = await fetchUser(1);
    console.log(user.name);
}

Error Handling with try/catch

async function getWeather(city) {
    try {
        const response = await fetch(`https://api.weather.com/current?city=${city}`);
        
        if (!response.ok) {
            throw new Error(`API returned ${response.status}`);
        }
        
        const data = await response.json();
        return data;
        
    } catch (error) {
        console.error(`Failed to get weather for ${city}:`, error.message);
        return null;   // Return fallback instead of crashing
    }
}

This is cleaner than .then().catch() chains, especially when you have multiple async operations that each might fail.

Sequential vs Parallel Execution

This is the most important performance concept with async/await:

// SEQUENTIAL — each waits for the previous to complete (~3 seconds total)
async function loadDashboard_slow() {
    const user    = await fetchUser(1);     // 1 second
    const orders  = await fetchOrders(1);  // 1 second
    const analytics = await fetchAnalytics(1); // 1 second
    return { user, orders, analytics };
}

// PARALLEL — all start at the same time (~1 second total)
async function loadDashboard_fast() {
    const [user, orders, analytics] = await Promise.all([
        fetchUser(1),
        fetchOrders(1),
        fetchAnalytics(1),
    ]);
    return { user, orders, analytics };
}

Rule: If requests don't depend on each other, run them in parallel with Promise.all. Sequential is for when you need the result of one to make the next request.

Real-World Patterns

Fetching with Full Error Handling

async function fetchWithHandling(url) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 5000);  // 5s timeout
    
    try {
        const response = await fetch(url, { signal: controller.signal });
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        return await response.json();
        
    } catch (error) {
        if (error.name === 'AbortError') {
            throw new Error('Request timed out after 5 seconds');
        }
        throw error;
    } finally {
        clearTimeout(timeoutId);
    }
}

Retry Logic

async function fetchWithRetry(url, maxRetries = 3) {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            return await response.json();
        } catch (error) {
            if (attempt === maxRetries) throw error;
            
            const delay = attempt * 1000;  // Exponential backoff: 1s, 2s, 3s
            console.log(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
}

Async Iteration

// Process items one at a time (useful to avoid rate limits)
async function processItems(items) {
    const results = [];
    
    for (const item of items) {
        const result = await processItem(item);
        results.push(result);
    }
    
    return results;
}

// Process items in batches of N (parallel within batch, sequential between)
async function processInBatches(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(processItem));
        results.push(...batchResults);
    }
    
    return results;
}

Async in React

// React: fetch data on component mount
function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
        async function loadUser() {
            try {
                setLoading(true);
                const data = await fetchUser(userId);
                setUser(data);
            } catch (err) {
                setError(err.message);
            } finally {
                setLoading(false);
            }
        }
        
        loadUser();
    }, [userId]);
    
    if (loading) return <Spinner />;
    if (error) return <Error message={error} />;
    return <UserCard user={user} />;
}

Common Mistakes

Mistake 1: Missing await

// BUG: data is a Promise, not the actual data
async function getUser(id) {
    const data = fetch(`/api/users/${id}`).then(r => r.json());  // forgot await
    console.log(data);  // Promise { <pending> }
}

// FIX:
async function getUser(id) {
    const data = await fetch(`/api/users/${id}`).then(r => r.json());
    console.log(data);  // The actual user object
}

Mistake 2: Unnecessary sequential awaits

// SLOW: these could run in parallel
const a = await getA();
const b = await getB();  // b waits for a, even though it doesn't need to

// FAST:
const [a, b] = await Promise.all([getA(), getB()]);

Mistake 3: async forEach

const ids = [1, 2, 3, 4, 5];

// BUG: forEach doesn't wait for async callbacks
ids.forEach(async (id) => {
    await processId(id);  // These all run concurrently but forEach returns before they complete
});

// FIX option 1: for...of (sequential)
for (const id of ids) {
    await processId(id);
}

// FIX option 2: Promise.all (parallel)
await Promise.all(ids.map(id => processId(id)));

async/await vs Promises — When to Use What

Use async/await for:

  • Most asynchronous code — it's more readable
  • Complex logic with multiple await points
  • Easy error handling with try/catch

Use Promises directly for:

  • Promise.all(), Promise.race(), Promise.allSettled()
  • When you need fine-grained control over the chain
  • Simple one-liners that don't need error handling

They work together seamlessly — await works on any Promise.

Next lesson: Fetch API & Working with REST APIs — building real data-driven applications.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!