Asynchronous JavaScript
Handle delayed operations without blocking the UI.
callbackspromisesasync/awaitTable of Contents
- JavaScript Asynchronous Programming
- Why Asynchronous Programming?
- Understanding the Event Loop
- Callbacks (The Old Way)
- Promises (The Modern Foundation)
- Async/Await (The Elegant Solution)
- Error Handling Strategies
- Parallel vs Sequential Execution
- Real-World Examples
- Common Pitfalls and Best Practices
- Interview Q&A
- MCQ
JavaScript Asynchronous Programming: A Complete Tutorial
Asynchronous programming allows JavaScript to perform long-running tasks (API calls, file operations, user events) without freezing the UI. This is essential because JavaScript is single-threaded.
This tutorial covers callbacks, promises, async/await, error handling, performance patterns, and practical async architecture.
1. Why Asynchronous Programming?
Synchronous code blocks the thread; asynchronous code keeps the app responsive.
Synchronous (Blocking)
console.log("Task 1: Start");
function waitThreeSeconds() {
const start = Date.now();
while (Date.now() - start < 3000) {}
console.log("Task 2: Done waiting");
}
waitThreeSeconds();
console.log("Task 3: This waits 3 seconds to run!");Asynchronous (Non-Blocking)
console.log("Task 1: Start");
setTimeout(() => {
console.log("Task 2: Done waiting (after 3 seconds)");
}, 3000);
console.log("Task 3: This runs immediately!");| Operation | Sync (Blocking) | Async (Non-Blocking) |
|---|---|---|
| Network request | Frozen UI | Responsive UI + loader |
| File operation | App hangs | Progress updates |
| DB query | Server stalls | Concurrent requests continue |
| Image processing | Tab unresponsive | User can still interact |
2. Understanding the Event Loop
Event loop coordinates call stack, Web APIs, microtask queue, and task queue.
console.log("1: Synchronous code");
setTimeout(() => console.log("4: Task Queue callback"), 0);
Promise.resolve().then(() => console.log("3: Microtask callback"));
console.log("2: More synchronous code");
// Output: 1, 2, 3, 4Priority System
console.log("Start");
setTimeout(() => console.log("Timeout (Macro Task)"), 0);
Promise.resolve().then(() => console.log("Promise (Micro Task)"));
console.log("End");Visualizing Event Loop
function simulateEventLoop() {
console.log("Main Stack: Start");
setTimeout(() => console.log("Macro Task: setTimeout"), 0);
Promise.resolve().then(() => console.log("Micro Task: Promise"));
console.log("Main Stack: End");
}
simulateEventLoop();3. Callbacks (The Old Way)
Basic Callback
function fetchUser(callback) {
setTimeout(() => callback({ id: 1, name: "Alice" }), 2000);
}
fetchUser(user => console.log(`User found: ${user.name}`));Callback Hell
getUser(1, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0], (details) => {
getPaymentInfo(details.id, (payment) => {
console.log("Final result:", payment);
});
});
});
});Inversion of Control Problem
function unreliableAsync(callback) {
if (Math.random() > 0.5) callback("Success");
// maybe never called, maybe called twice
}4. Promises (The Modern Foundation)
Promise States
const pendingPromise = new Promise(() => {});
const fulfilledPromise = Promise.resolve("Success!");
const rejectedPromise = Promise.reject(new Error("Failed!"));Create and Use Promises
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => id <= 0 ? reject(new Error("Invalid user ID")) : resolve({ id, name: `User${id}` }), 1000);
});
}
fetchUser(1)
.then(user => user.name)
.then(name => console.log(name.toUpperCase()))
.catch(error => console.error(error.message))
.finally(() => console.log("Operation completed"));Promise Chaining
getUser(1)
.then(user => getOrders(user))
.then(orders => getOrderDetails(orders[0]))
.then(details => console.log(details))
.catch(error => console.error(error));Promise Methods
Promise.all([fetchUser(), fetchPosts(), fetchComments()]);
Promise.allSettled([successful, failing, slow]);
Promise.race([fetch(url), timeoutPromise]);
Promise.any([backup1, backup2, backup3]);5. Async/Await (The Elegant Solution)
async function getUserSummary(userId) {
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);
return { userName: user.name, orderCount: orders.length };
}Promise Chain to Async/Await
async function getData() {
try {
const user = await fetchUser(1);
const orders = await fetchOrders(user.id);
const details = await fetchOrderDetails(orders[0].id);
return await processPayment(details.total);
} catch (error) {
handleError(error);
}
}Loops and Async
// Sequential
for (const url of urls) {
const response = await fetch(url);
}
// Parallel
await Promise.all(urls.map(url => fetch(url).then(r => r.json())));Async in Array Methods
// Avoid forEach with await
for (const user of users) {
const orders = await fetchOrders(user.id);
}
// Or parallel:
await Promise.all(users.map(async user => ({ name: user.name, orders: await fetchOrders(user.id) })));6. Error Handling Strategies
Try/Catch
async function robustFetchUser(id) {
try {
return await fetchUser(id);
} catch (error) {
return { id: null, name: "Unknown", isFallback: true };
}
}Specific Error Handling
try {
await processPayment();
} catch (error) {
if (error.message.includes("Network")) return { status: "retry" };
if (error.message.includes("Authentication")) throw error;
return { status: "failed", error: error.message };
}Global Handlers
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled Promise rejection:", event.reason);
});7. Parallel vs Sequential Execution
Sequential
const user = await fetchUser(1);
const orders = await fetchOrders(user.id);
const details = await fetchOrderDetails(orders[0].id);Parallel
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);Batch Processing
async function batchProcess(items, batchSize = 3) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
results.push(...await Promise.all(batch.map(item => processItem(item))));
}
return results;
}8. Real-World Examples
Example 1: Weather Dashboard
class WeatherService {
async getWeather(city) {
try {
const response = await fetch(`https://api.weather.com/${city}`);
if (!response.ok) throw new Error(`Weather API error: ${response.status}`);
const data = await response.json();
return { city: data.name, temperature: data.main.temp };
} catch (error) {
return { city, temperature: "N/A", condition: "Unavailable" };
}
}
}Example 2: File Upload with Progress
class FileUploader {
async uploadFile(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", e => onProgress?.((e.loaded / e.total) * 100));
xhr.addEventListener("load", () => xhr.status === 200 ? resolve(JSON.parse(xhr.responseText)) : reject(new Error("Upload failed")));
xhr.addEventListener("error", () => reject(new Error("Network error")));
xhr.open("POST", "/upload");
const formData = new FormData(); formData.append("file", file); xhr.send(formData);
});
}
}Example 3: Infinite Scroll with Debouncing
class InfiniteScroll {
constructor(loadMoreItems) { this.loadMore = loadMoreItems; this.isLoading = false; this.page = 1; }
debounce(func, delay) { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => func.apply(this, a), delay); }; }
async fetchMore() { if (this.isLoading) return; this.isLoading = true; try { const items = await this.loadMore(this.page); this.page++; } finally { this.isLoading = false; } }
}Example 4: Retry with Exponential Backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
lastError = error;
if (attempt === maxRetries) break;
await new Promise(r => setTimeout(r, Math.pow(2, attempt - 1) * 1000));
}
}
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}9. Common Pitfalls and Best Practices
Pitfall 1: Forgetting await
// Wrong
const user = fetchUser(1);
// Correct
const realUser = await fetchUser(1);Pitfall 2: Sequential loop when parallel needed
// Better for independent tasks:
await Promise.all(items.map(item => processItem(item)));Pitfall 3: Unhandled rejections
try {
const data = await fetch("https://api.example.com");
return await data.json();
} catch (error) {
console.error("API call failed:", error);
return null;
}Pitfall 4: Race conditions
let activeRequestId = 0;
async function loadUser(userId) {
const requestId = ++activeRequestId;
const user = await fetchUser(userId);
if (requestId === activeRequestId) displayUser(user);
}Best Practices Summary
| Do | Don't |
|---|---|
| Handle rejections with try/catch | Ignore async errors |
| Use Promise.all for independent tasks | Await everything sequentially by default |
| Use finally for cleanup | Leave loaders/connections hanging |
| Use timeouts and retries for network calls | Let requests hang forever |
| Use meaningful async names | Hide intent in vague variables |
Related: How JS Works, Modules, Error Handling.