Asynchronous JavaScript

Master callbacks, promises, and async/await

Callbacks

Understanding Callbacks

// Basic Callback Example
function fetchData(callback) {
    setTimeout(() => {
        const data = { id: 1, name: "John Doe" };
        callback(null, data); // First parameter is error, second is data
    }, 1000);
}

fetchData((error, data) => {
    if (error) {
        console.error("Error:", error);
    } else {
        console.log("Data received:", data);
    }
});

// Callback Hell Example
function getUserData(userId, callback) {
    fetchUser(userId, (err, user) => {
        if (err) return callback(err);
        
        fetchUserPosts(user.id, (err, posts) => {
            if (err) return callback(err);
            
            fetchPostComments(posts[0].id, (err, comments) => {
                if (err) return callback(err);
                
                callback(null, { user, posts, comments });
            });
        });
    });
}

// Error-First Callback Pattern
function readFile(filename, callback) {
    // Simulate file reading
    setTimeout(() => {
        if (filename.endsWith('.txt')) {
            callback(null, "File content here");
        } else {
            callback(new Error("Invalid file type"));
        }
    }, 500);
}

⚠️ Callback Hell Problems

  • • Deeply nested code that's hard to read
  • • Difficult error handling
  • • Hard to maintain and debug
  • • Inversion of control issues

Promises

Promise Basics

// Creating a Promise
function fetchUserData(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId > 0) {
                resolve({ id: userId, name: `User ${userId}` });
            } else {
                reject(new Error("Invalid user ID"));
            }
        }, 1000);
    });
}

// Using Promises
fetchUserData(1)
    .then(user => {
        console.log("User:", user);
        return fetchUserPosts(user.id);
    })
    .then(posts => {
        console.log("Posts:", posts);
        return fetchPostComments(posts[0].id);
    })
    .then(comments => {
        console.log("Comments:", comments);
    })
    .catch(error => {
        console.error("Error:", error);
    })
    .finally(() => {
        console.log("Operation completed");
    });

// Promise.all - Wait for all promises
const promises = [
    fetchUserData(1),
    fetchUserData(2),
    fetchUserData(3)
];

Promise.all(promises)
    .then(users => {
        console.log("All users:", users);
    })
    .catch(error => {
        console.error("One or more requests failed:", error);
    });

// Promise.allSettled - Wait for all, regardless of outcome
Promise.allSettled(promises)
    .then(results => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`User ${index + 1}:`, result.value);
            } else {
                console.error(`User ${index + 1} failed:`, result.reason);
            }
        });
    });

// Promise.race - First to complete wins
Promise.race([
    fetchUserData(1),
    new Promise((_, reject) => 
        setTimeout(() => reject(new Error("Timeout")), 2000)
    )
])
.then(result => console.log("First result:", result))
.catch(error => console.error("Error or timeout:", error));

Async/Await

Modern Async Syntax

// Basic Async/Await
async function getUserData(userId) {
    try {
        const user = await fetchUserData(userId);
        const posts = await fetchUserPosts(user.id);
        const comments = await fetchPostComments(posts[0].id);
        
        return { user, posts, comments };
    } catch (error) {
        console.error("Error fetching user data:", error);
        throw error; // Re-throw if needed
    }
}

// Using the async function
async function main() {
    try {
        const userData = await getUserData(1);
        console.log("Complete user data:", userData);
    } catch (error) {
        console.error("Failed to get user data:", error);
    }
}

// Parallel Execution with Async/Await
async function fetchMultipleUsers() {
    try {
        // Sequential (slower)
        const user1 = await fetchUserData(1);
        const user2 = await fetchUserData(2);
        const user3 = await fetchUserData(3);
        
        // Parallel (faster)
        const [user1, user2, user3] = await Promise.all([
            fetchUserData(1),
            fetchUserData(2),
            fetchUserData(3)
        ]);
        
        return [user1, user2, user3];
    } catch (error) {
        console.error("Error fetching users:", error);
        throw error;
    }
}

// Async/Await with Error Handling
async function robustApiCall() {
    const maxRetries = 3;
    let retries = 0;
    
    while (retries < maxRetries) {
        try {
            const response = await fetch('/api/data');
            
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            
            const data = await response.json();
            return data;
        } catch (error) {
            retries++;
            
            if (retries === maxRetries) {
                throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
            }
            
            // Wait before retrying
            await new Promise(resolve => setTimeout(resolve, 1000 * retries));
        }
    }
}

// Async Generators
async function* fetchUsersPaginated() {
    let page = 1;
    let hasMore = true;
    
    while (hasMore) {
        const response = await fetch(`/api/users?page=${page}`);
        const data = await response.json();
        
        yield data.users;
        
        hasMore = data.hasMore;
        page++;
    }
}

// Using async generator
async function processAllUsers() {
    for await (const userBatch of fetchUsersPaginated()) {
        console.log(`Processing ${userBatch.length} users`);
        // Process each batch
    }
}

Fetch API

Modern HTTP Requests

// Basic GET Request
async function fetchUsers() {
    try {
        const response = await fetch('/api/users');
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const users = await response.json();
        return users;
    } catch (error) {
        console.error('Fetch error:', error);
        throw error;
    }
}

// POST Request with JSON
async function createUser(userData) {
    try {
        const response = await fetch('/api/users', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${getAuthToken()}`
            },
            body: JSON.stringify(userData)
        });
        
        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(errorData.message || 'Failed to create user');
        }
        
        return await response.json();
    } catch (error) {
        console.error('Error creating user:', error);
        throw error;
    }
}

// File Upload
async function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);
    
    try {
        const response = await fetch('/api/upload', {
            method: 'POST',
            body: formData // Don't set Content-Type header for FormData
        });
        
        if (!response.ok) {
            throw new Error('Upload failed');
        }
        
        return await response.json();
    } catch (error) {
        console.error('Upload error:', error);
        throw error;
    }
}

// Request with Timeout
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);
    
    try {
        const response = await fetch(url, {
            ...options,
            signal: controller.signal
        });
        
        clearTimeout(timeoutId);
        return response;
    } catch (error) {
        clearTimeout(timeoutId);
        
        if (error.name === 'AbortError') {
            throw new Error('Request timeout');
        }
        
        throw error;
    }
}

// Retry Logic
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
    for (let i = 0; i <= maxRetries; i++) {
        try {
            const response = await fetch(url, options);
            
            if (response.ok) {
                return response;
            }
            
            // Don't retry on client errors (4xx)
            if (response.status >= 400 && response.status < 500) {
                throw new Error(`Client error: ${response.status}`);
            }
            
            if (i === maxRetries) {
                throw new Error(`Server error: ${response.status}`);
            }
        } catch (error) {
            if (i === maxRetries) {
                throw error;
            }
            
            // Exponential backoff
            await new Promise(resolve => 
                setTimeout(resolve, Math.pow(2, i) * 1000)
            );
        }
    }
}

Understanding the Event Loop

💡 How JavaScript Handles Async

Call Stack: Where function calls are executed

Web APIs: Browser APIs (setTimeout, fetch, DOM events)

Callback Queue: Where callbacks wait to be executed

Event Loop: Moves callbacks from queue to stack when stack is empty

Event Loop Example

console.log('1'); // Synchronous

setTimeout(() => {
    console.log('2'); // Macro task
}, 0);

Promise.resolve().then(() => {
    console.log('3'); // Micro task
});

console.log('4'); // Synchronous

// Output: 1, 4, 3, 2
// Explanation:
// 1. Synchronous code runs first (1, 4)
// 2. Micro tasks (Promises) run before macro tasks (setTimeout)
// 3. So Promise callback (3) runs before setTimeout callback (2)

// Micro tasks vs Macro tasks
setTimeout(() => console.log('macro 1'), 0);
setTimeout(() => console.log('macro 2'), 0);

Promise.resolve().then(() => console.log('micro 1'));
Promise.resolve().then(() => console.log('micro 2'));

// Output: micro 1, micro 2, macro 1, macro 2

Async Best Practices

✅ Do

  • • Use async/await for cleaner code
  • • Handle errors with try/catch
  • • Use Promise.all for parallel execution
  • • Implement proper error boundaries
  • • Use AbortController for cancellation

❌ Don't

  • • Forget to handle promise rejections
  • • Use async/await in loops unnecessarily
  • • Mix callbacks and promises
  • • Ignore network timeouts
  • • Block the main thread with heavy operations