JavaScript Intermediate

JavaScript Async Programming: Promises, Async/Await, and APIs

CodingerWeb
CodingerWeb
20 views 75 min read

Understanding Asynchronous JavaScript

Asynchronous programming allows JavaScript to perform tasks without blocking the main thread. This is essential for web applications that need to fetch data, handle user interactions, and perform time-consuming operations.

What is Asynchronous Programming?

// Synchronous code (blocking)
console.log("First");
console.log("Second");
console.log("Third");
// Output: First, Second, Third (in order)

// Asynchronous code (non-blocking)
console.log("First");
setTimeout(() => {
    console.log("Second (delayed)");
}, 1000);
console.log("Third");
// Output: First, Third, Second (delayed) - Third doesn't wait!

Callbacks (Traditional Approach)

// Basic callback
function fetchData(callback) {
    setTimeout(() => {
        let data = { id: 1, name: "John Doe" };
        callback(data);
    }, 1000);
}

fetchData(function(result) {
    console.log("Received data:", result);
});

// Callback hell (problematic)
function step1(callback) {
    setTimeout(() => callback("Step 1 complete"), 1000);
}

function step2(callback) {
    setTimeout(() => callback("Step 2 complete"), 1000);
}

function step3(callback) {
    setTimeout(() => callback("Step 3 complete"), 1000);
}

// This becomes hard to read and maintain
step1(function(result1) {
    console.log(result1);
    step2(function(result2) {
        console.log(result2);
        step3(function(result3) {
            console.log(result3);
            console.log("All steps complete!");
        });
    });
});

Promises (Modern Approach)

// Creating a Promise
function fetchUserData(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId > 0) {
                resolve({ id: userId, name: "Alice", email: "alice@example.com" });
            } else {
                reject(new Error("Invalid user ID"));
            }
        }, 1000);
    });
}

// Using Promises with .then() and .catch()
fetchUserData(1)
    .then(user => {
        console.log("User found:", user);
        return user.id;  // Return value for next .then()
    })
    .then(userId => {
        console.log("User ID:", userId);
    })
    .catch(error => {
        console.error("Error:", error.message);
    })
    .finally(() => {
        console.log("Promise completed");
    });

// Handle rejection
fetchUserData(-1)
    .then(user => console.log(user))
    .catch(error => console.error("Caught error:", error.message));

Async/Await (Most Modern Approach)

// Convert Promise-based code to async/await
async function getUserData(userId) {
    try {
        let user = await fetchUserData(userId);
        console.log("User found:", user);
        
        // You can use the result immediately
        let userId = user.id;
        console.log("User ID:", userId);
        
        return user;  // This returns a Promise
    } catch (error) {
        console.error("Error:", error.message);
        throw error;  // Re-throw if needed
    } finally {
        console.log("Function completed");
    }
}

// Call async function
getUserData(1)
    .then(user => console.log("Final result:", user))
    .catch(error => console.error("Final error:", error));

// Or use await in another async function
async function main() {
    try {
        let user = await getUserData(1);
        console.log("Got user in main:", user);
    } catch (error) {
        console.error("Error in main:", error);
    }
}

main();

Fetching Data from APIs

// Using fetch() API with Promises
function getWeatherData(city) {
    let apiKey = "your-api-key";
    let url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}`;
    
    return fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            return response.json();
        })
        .then(data => {
            return {
                city: data.name,
                temperature: Math.round(data.main.temp - 273.15), // Convert from Kelvin
                description: data.weather[0].description
            };
        });
}

// Using the weather function
getWeatherData("London")
    .then(weather => {
        console.log(`Weather in ${weather.city}: ${weather.temperature}°C, ${weather.description}`);
    })
    .catch(error => {
        console.error("Weather fetch error:", error);
    });

// Same function with async/await
async function getWeatherDataAsync(city) {
    try {
        let apiKey = "your-api-key";
        let url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}`;
        
        let response = await fetch(url);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        let data = await response.json();
        
        return {
            city: data.name,
            temperature: Math.round(data.main.temp - 273.15),
            description: data.weather[0].description
        };
    } catch (error) {
        console.error("Weather fetch error:", error);
        throw error;
    }
}

Working with Multiple Promises

// Promise.all - wait for all promises to complete
async function fetchMultipleUsers() {
    try {
        let promises = [
            fetchUserData(1),
            fetchUserData(2),
            fetchUserData(3)
        ];
        
        let users = await Promise.all(promises);
        console.log("All users:", users);
        return users;
    } catch (error) {
        console.error("One or more requests failed:", error);
    }
}

// Promise.allSettled - get results of all promises (even if some fail)
async function fetchUsersWithResults() {
    let promises = [
        fetchUserData(1),
        fetchUserData(-1),  // This will fail
        fetchUserData(3)
    ];
    
    let results = await Promise.allSettled(promises);
    
    results.forEach((result, index) => {
        if (result.status === "fulfilled") {
            console.log(`User ${index + 1}:`, result.value);
        } else {
            console.log(`User ${index + 1} failed:`, result.reason.message);
        }
    });
}

// Promise.race - return first promise to complete
async function fetchFastestResponse() {
    let promises = [
        fetch("https://api1.example.com/data"),
        fetch("https://api2.example.com/data"),
        fetch("https://api3.example.com/data")
    ];
    
    try {
        let fastestResponse = await Promise.race(promises);
        let data = await fastestResponse.json();
        console.log("Fastest response:", data);
    } catch (error) {
        console.error("All requests failed or fastest failed:", error);
    }
}

Error Handling in Async Code

// Comprehensive error handling
async function robustApiCall(url) {
    const maxRetries = 3;
    let lastError;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            console.log(`Attempt ${attempt} of ${maxRetries}`);
            
            let response = await fetch(url);
            
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            
            let data = await response.json();
            console.log("Success on attempt", attempt);
            return data;
            
        } catch (error) {
            lastError = error;
            console.log(`Attempt ${attempt} failed:`, error.message);
            
            if (attempt < maxRetries) {
                // Wait before retrying (exponential backoff)
                let delay = Math.pow(2, attempt) * 1000;
                console.log(`Waiting ${delay}ms before retry...`);
                await new Promise(resolve => setTimeout(resolve, delay));
            }
        }
    }
    
    throw new Error(`All ${maxRetries} attempts failed. Last error: ${lastError.message}`);
}

// Usage with proper error handling
async function handleApiCall() {
    try {
        let data = await robustApiCall("https://jsonplaceholder.typicode.com/posts/1");
        console.log("Final result:", data);
    } catch (error) {
        console.error("Final failure:", error.message);
        // Show user-friendly error message
        alert("Sorry, we couldn't load the data. Please try again later.");
    }
}

Practical Example: Building a Simple API Client

class ApiClient {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
    }
    
    async request(endpoint, options = {}) {
        let url = `${this.baseUrl}${endpoint}`;
        
        let config = {
            headers: {
                "Content-Type": "application/json",
                ...options.headers
            },
            ...options
        };
        
        try {
            let response = await fetch(url, config);
            
            if (!response.ok) {
                throw new Error(`API Error: ${response.status} ${response.statusText}`);
            }
            
            return await response.json();
        } catch (error) {
            console.error("API request failed:", error);
            throw error;
        }
    }
    
    async get(endpoint) {
        return this.request(endpoint, { method: "GET" });
    }
    
    async post(endpoint, data) {
        return this.request(endpoint, {
            method: "POST",
            body: JSON.stringify(data)
        });
    }
    
    async put(endpoint, data) {
        return this.request(endpoint, {
            method: "PUT",
            body: JSON.stringify(data)
        });
    }
    
    async delete(endpoint) {
        return this.request(endpoint, { method: "DELETE" });
    }
}

// Usage example
async function demonstrateApiClient() {
    let api = new ApiClient("https://jsonplaceholder.typicode.com");
    
    try {
        // GET request
        let posts = await api.get("/posts?_limit=5");
        console.log("Posts:", posts);
        
        // POST request
        let newPost = await api.post("/posts", {
            title: "My New Post",
            body: "This is the content of my new post",
            userId: 1
        });
        console.log("Created post:", newPost);
        
        // PUT request
        let updatedPost = await api.put("/posts/1", {
            id: 1,
            title: "Updated Post Title",
            body: "Updated content",
            userId: 1
        });
        console.log("Updated post:", updatedPost);
        
    } catch (error) {
        console.error("API demonstration failed:", error);
    }
}

// Run the demonstration
demonstrateApiClient();