正在加载,请稍候…

JavaScript Promises and Async/Await: The Complete Visual Guide

Master JavaScript asynchronous programming with Promises and async/await. Learn common patterns, error handling, parallel execution, and how to avoid the most frequent mistakes.

JavaScript Promises and Async/Await: The Complete Visual Guide

Async programming is one of the hardest concepts to master in JavaScript. This guide covers everything from callbacks to Promises to async/await — with visual explanations and real-world patterns.

The Problem: Callback Hell

// The old way — "callback pyramid of doom"
getUser(userId, function(err, user) {
  if (err) return handleError(err);
  getOrders(user.id, function(err, orders) {
    if (err) return handleError(err);
    getProducts(orders[0].id, function(err, products) {
      if (err) return handleError(err);
      // Now you're 3 levels deep...
      displayResults(user, orders, products);
    });
  });
});

Promises and async/await solve this.

Understanding Promises

A Promise is an object representing the eventual completion (or failure) of an async operation.

// Creating a promise
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = Math.random() > 0.5;
    if (success) {
      resolve('Data loaded!');  // Success
    } else {
      reject(new Error('Failed to load'));  // Failure
    }
  }, 1000);
});

Three states:

  1. Pending — initial state, operation in progress
  2. Fulfilled — operation completed successfully
  3. Rejected — operation failed

Promise Chaining

// Flat, readable chain
fetchUser(userId)
  .then(user => fetchOrders(user.id))     // Each .then receives previous result
  .then(orders => fetchProducts(orders))
  .then(products => displayProducts(products))
  .catch(error => console.error('Something failed:', error))
  .finally(() => setLoading(false));       // Always runs

Key rule: .then() always returns a new Promise, so you can chain infinitely.

Async/Await — Cleaner Syntax

// Same logic, but reads like synchronous code
async function loadUserData(userId) {
  try {
    const user = await fetchUser(userId);
    const orders = await fetchOrders(user.id);
    const products = await fetchProducts(orders);
    displayProducts(products);
  } catch (error) {
    console.error('Something failed:', error);
  } finally {
    setLoading(false);
  }
}

Rules of async/await:

  • async function always returns a Promise
  • await can only be used inside async functions
  • await pauses execution until the Promise resolves

Parallel Execution — The Most Important Pattern

Sequential (slow) — awaiting one at a time:

// This takes 3 seconds if each request takes 1 second
async function loadDashboard() {
  const users = await fetchUsers();     // 1s
  const orders = await fetchOrders();   // 1s  
  const stats = await fetchStats();     // 1s
  return { users, orders, stats };      // Total: 3s
}

Parallel (fast) — run simultaneously:

// This takes 1 second (all run at the same time)
async function loadDashboard() {
  const [users, orders, stats] = await Promise.all([
    fetchUsers(),
    fetchOrders(),
    fetchStats(),
  ]);
  return { users, orders, stats };  // Total: 1s
}

When to use which:

  • Use Promise.all when requests are independent
  • Use sequential await when request B depends on result of request A

Promise Methods Explained

Promise.all — All or Nothing

// Resolves when ALL resolve; rejects if ANY reject
const [user, profile, settings] = await Promise.all([
  fetchUser(id),
  fetchProfile(id),
  fetchSettings(id),
]);

Promise.allSettled — Get All Results

// Resolves when ALL complete (regardless of success/failure)
const results = await Promise.allSettled([
  fetchUser(id),
  fetchProfile(id),
  fetchSettings(id),
]);

results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log('Success:', result.value);
  } else {
    console.log('Failed:', result.reason);
  }
});

Promise.race — First Wins

// Resolves/rejects with the FIRST settled promise
// Useful for timeouts
const result = await Promise.race([
  fetchData(),
  new Promise((_, reject) => 
    setTimeout(() => reject(new Error('Timeout')), 5000)
  )
]);

Promise.any — First Success

// Resolves with first FULFILLED promise
// Rejects only if ALL reject (AggregateError)
const result = await Promise.any([
  fetchFromServer1(),
  fetchFromServer2(),
  fetchFromServer3(),
]);
// Returns whichever server responds first successfully

Error Handling Patterns

The Try/Catch Pattern

async function fetchUserSafe(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Request was cancelled');
    } else if (error.name === 'TypeError') {
      console.log('Network error — are you online?');
    } else {
      console.error('Unexpected error:', error);
    }
    return null; // Or re-throw, depending on your needs
  }
}

Result Pattern (Go-style error handling)

// Returns [data, error] tuple — no try/catch needed at call site
async function safeAsync(promise) {
  try {
    const data = await promise;
    return [data, null];
  } catch (error) {
    return [null, error];
  }
}

// Usage
const [user, error] = await safeAsync(fetchUser(id));
if (error) {
  console.error('Failed:', error);
  return;
}
// user is guaranteed to be defined here
console.log(user.name);

Common Mistakes

Mistake 1: Not Awaiting in Loops

// ❌ Wrong — all requests fire simultaneously, won't catch errors properly
async function processItems(items) {
  items.forEach(async (item) => {
    await processItem(item); // forEach doesn't await async callbacks!
  });
}

// ✅ Correct — sequential processing
async function processItemsSequential(items) {
  for (const item of items) {
    await processItem(item);
  }
}

// ✅ Correct — parallel processing
async function processItemsParallel(items) {
  await Promise.all(items.map(item => processItem(item)));
}

Mistake 2: Missing Await

// ❌ Wrong — console.log gets a Promise object, not the data
async function getUser() {
  const user = fetchUser(); // Missing await!
  console.log(user); // Promise { <pending> }
}

// ✅ Correct
async function getUser() {
  const user = await fetchUser();
  console.log(user); // { id: 1, name: 'John' }
}

Mistake 3: Unhandled Promise Rejections

// ❌ Dangerous — unhandled rejection can crash Node.js
async function riskyOperation() {
  await doSomething();
}
riskyOperation(); // No .catch() anywhere!

// ✅ Always handle rejections
riskyOperation().catch(console.error);
// Or use top-level await in modules
try {
  await riskyOperation();
} catch (e) {
  console.error(e);
}

Mistake 4: Creating Unnecessary Promises

// ❌ Redundant — wrapping an async function in a new Promise
function fetchData() {
  return new Promise(async (resolve, reject) => {
    try {
      const data = await fetch('/api/data');
      resolve(data);
    } catch (error) {
      reject(error);
    }
  });
}

// ✅ Async functions already return Promises
async function fetchData() {
  const data = await fetch('/api/data');
  return data; // Automatically wrapped in a Promise
}

Real-World Pattern: Data Fetching with Retry

async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const response = await fetch(url, options);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (attempt === retries) throw error;
      
      // Exponential backoff: wait 1s, 2s, 4s...
      const delay = Math.pow(2, attempt - 1) * 1000;
      console.log(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Usage
const data = await fetchWithRetry('https://api.example.com/data');

Summary

Pattern Use When
await sequentially Each step depends on the previous
Promise.all Independent operations, need all results
Promise.allSettled Need all results, some may fail
Promise.race Timeout pattern, first result wins
Promise.any Multiple sources, need first success

→ Test JSON API responses with JSON Viewer and decode JWT tokens at JWT Parser.