Concurrency Có Cấu Trúc trong JavaScript: Vượt Qua Promise.all

07 tháng 4, 2026·5 phút đọc

Bài viết phân tích các vấn đề thường gặp khi xử lý bất đồng bộ trong JavaScript và giới thiệu các kỹ thuật concurrency có cấu trúc như Promise.allSettled, AbortController, Promise.race, Promise.any và mô hình giới hạn concurrency để tối ưu hiệu quả xử lý.

Concurrency Có Cấu Trúc trong JavaScript: Vượt Qua Promise.all

Concurrency Có Cấu Trúc trong JavaScript: Vượt Qua Promise.all

JavaScript ngày càng phổ biến trong phát triển web và ứng dụng nhờ tính năng bất đồng bộ (async) giúp tối ưu trải nghiệm người dùng. Tuy nhiên, việc quản lý hàng loạt tác vụ bất đồng bộ một cách hiệu quả và an toàn không hề đơn giản. Bài viết này đề cập đến các vấn đề của async code truyền thống và giới thiệu các kỹ thuật concurrency có cấu trúc giúp xử lý concurrency một cách rõ ràng và kiểm soát tốt hơn trong JavaScript.

Vấn đề với async không có cấu trúc

Trong JavaScript, các Promise được khởi tạo chạy song song nhưng không có phạm vi quản lý rõ ràng. Nếu một Promise trong số đó bị lỗi (reject), Promise.all sẽ trả về lỗi, nhưng các Promise khác tiếp tục chạy mà không bị hủy. Điều này dẫn đến các tác vụ chạy dở gây hao tốn tài nguyên, thậm chí có thể gây ra các hành động không mong muốn như ghi dữ liệu lỗi thời.

async function loadDashboard(userId: string) {
  const [user, orders, analytics] = await Promise.all([
    getUser(userId),
    getOrders(userId),     // Lỗi xảy ra sau 2 giây
    getAnalytics(userId),  // Vẫn chạy tiếp
  ]);
  // getAnalytics không bị hủy khi getOrders lỗi
}

Khi getOrders từ chối (reject), Promise.all cũng từ chối, nhưng getAnalytics vẫn chạy ngầm hậu trường, không được hủy bỏ.

Promise.allSettled: Xử lý kết quả từng Promise

Promise.allSettled trả về mảng kết quả với trạng thái từng Promise, cho phép bạn xử lý độc lập từng kết quả thành công hoặc lỗi, không bị ảnh hưởng bởi Promise khác.

async function loadDashboard(userId: string) {
  const results = await Promise.allSettled([
    getUser(userId),
    getOrders(userId),
    getAnalytics(userId),
  ]);
  const [userResult, ordersResult, analyticsResult] = results;

  const user = userResult.status === 'fulfilled' ? userResult.value : null;
  const orders = ordersResult.status === 'fulfilled' ? ordersResult.value : [];
  if (analyticsResult.status === 'rejected') {
    console.error('Analytics failed:', analyticsResult.reason);
  }
  return {
    user,
    orders,
    analytics: analyticsResult.status === 'fulfilled' ? analyticsResult.value : null
  };
}

Cách này xử lý từng Promise nhẹ nhàng hơn, tránh trường hợp lỗi làm hỏng toàn bộ khối lệnh.

AbortController: Hủy request thực sự

JavaScript hỗ trợ AbortController để huỷ các tác vụ đang chạy như fetch API, phù hợp khi cần dọn dẹp tài nguyên hoặc hủy request khi component React unmount.

async function fetchWithTimeout<T>(url: string, timeoutMs: number): Promise<T> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const response = await fetch(url, { signal: controller.signal });
    return response.json();
  } finally {
    clearTimeout(timeoutId);
  }
}

// Trong React hook
function useUserData(userId: string) {
  const [data, setData] = useState(null);
  useEffect(() => {
    const controller = new AbortController();
    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(r => r.json())
      .then(setData)
      .catch(err => {
        if (err.name !== 'AbortError') throw err;
      });
    return () => controller.abort();
  }, [userId]);
  return data;
}

AbortController giúp tránh các thao tác thừa khi người dùng chuyển trang hoặc hủy tương tác.

Promise.race và Promise.any: Chiến thuật “ai về đích trước”

  • Promise.race trả về kết quả của Promise đầu tiên hoàn thành (thành công hoặc lỗi).
  • Promise.any trả về kết quả thành công đầu tiên, chỉ thất bại khi tất cả đều lỗi.
// Timeout với Promise.race
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

// Cache và database race
async function getWithFallback(key: string) {
  return Promise.race([
    redis.get(key).then(v => JSON.parse(v!)), // cache nhanh hơn
    db.slowQuery(key),                         // database chậm
  ]);
}

// Lấy nội dung từ nhiều CDN
async function fetchFromCDN(path: string) {
  return Promise.any([
    fetch(`https://cdn1.example.com${path}`),
    fetch(`https://cdn2.example.com${path}`),
    fetch(`https://cdn3.example.com${path}`),
  ]);
}

Những kỹ thuật này giúp tối ưu tốc độ bằng cách tận dụng nguồn dữ liệu hoặc tính toán song song.

Giới hạn concurrency: tránh quá tải tài nguyên

Chạy hàng nghìn tác vụ bất đồng bộ đồng thời có thể làm nghẽn API, database. Giải pháp là chạy theo lô (batch) hoặc giới hạn số tác vụ cùng lúc bằng thư viện như p-limit.

async function processInBatches<T, R>(
  items: T[],
  processor: (item: T) => Promise<R>,
  concurrency: number
): Promise<R[]> {
  const results: R[] = [];
  for (let i = 0; i < items.length; i += concurrency) {
    const batch = items.slice(i, i + concurrency);
    const batchResults = await Promise.all(batch.map(processor));
    results.push(...batchResults);
  }
  return results;
}

// Sử dụng p-limit
import pLimit from 'p-limit';
const limit = pLimit(10); // max 10 tasks chạy song song
const results = await Promise.all(
  users.map(user => limit(() => processUser(user)))
);

Điều này giúp cân bằng tải và tránh lỗi do quá tải dịch vụ.

Async Iteration: Xử lý dòng dữ liệu lớn hiệu quả

JavaScript hỗ trợ async iteration giúp bạn xử lý từng phần dữ liệu theo luồng mà không cần tải toàn bộ vào bộ nhớ.

async function* generateUsers(): AsyncGenerator<User> {
  let page = 1;
  while (true) {
    const users = await db.users.findMany({ skip: (page - 1) * 100, take: 100 });
    if (users.length === 0) return;
    yield* users;
    page++;
  }
}

for await (const user of generateUsers()) {
  await sendEmail(user.email);
}

Phương pháp phù hợp khi làm việc với tập dữ liệu lớn trên backend server.

Kết luận

Mỗi trường hợp concurrency trong JavaScript lại có cách xử lý tốt nhất riêng:

  • Promise.all: khi tất cả Promise cần phải thành công
  • Promise.allSettled: xử lý từng kết quả độc lập
  • Promise.race: lấy kết quả nhanh nhất, lỗi hay thành công đều được
  • Promise.any: lấy kết quả thành công đầu tiên, ignore lỗi
  • AbortController: huỷ request bất đồng bộ đang chạy
  • p-limit hoặc batch processing: giới hạn concurrency tránh quá tải

Tầm quan trọng của concurrency structured (có cấu trúc) là giúp code rõ ràng, dễ kiểm soát lỗi và dọn dẹp tài nguyên. Với xu hướng phức tạp hơn về async, các kỹ thuật này sẽ giúp lập trình JavaScript trở nên bền vững và hiệu quả hơn.

Bạn có thể tìm thấy các pattern concurrency này kèm xử lý lỗi tốt trong các bộ khởi đầu (starter kits) như Whoff Agents AI SaaS Starter Kit dành cho phát triển ứng dụng AI hoặc SaaS.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗