Asynchronous JavaScript

Handle delayed operations without blocking the UI.

callbackspromisesasync/await

Table of Contents

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!");
OperationSync (Blocking)Async (Non-Blocking)
Network requestFrozen UIResponsive UI + loader
File operationApp hangsProgress updates
DB queryServer stallsConcurrent requests continue
Image processingTab unresponsiveUser 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, 4

Priority 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

DoDon't
Handle rejections with try/catchIgnore async errors
Use Promise.all for independent tasksAwait everything sequentially by default
Use finally for cleanupLeave loaders/connections hanging
Use timeouts and retries for network callsLet requests hang forever
Use meaningful async namesHide intent in vague variables

Related: How JS Works, Modules, Error Handling.

10 Async JavaScript Interview Q&A

10 Async JavaScript MCQs