Table of Contents
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();