1. Promises là gì?

Promise là một đối tượng đại diện cho kết quả của một tác vụ bất đồng bộ. Nó có thể ở một trong ba trạng thái:

  • Pending: Đang chờ kết quả
  • Fulfilled: Hoàn thành thành công
  • Rejected: Hoàn thành thất bại
const promise = new Promise((resolve, reject) => {
  // Tác vụ bất đồng bộ
  setTimeout(() => {
    const random = Math.random();
    if (random > 0.5) {
      resolve("Success!");
    } else {
      reject("Failed!");
    }
  }, 1000);
});

promise
  .then((result) => console.log(result))
  .catch((error) => console.error(error));

2. Async/Await là gì?

Async/Await là cú pháp “syntactic sugar” giúp viết code bất đồng bộ theo kiểu đồng bộ, giúp code dễ đọc và maintain hơn.

async function example() {
  try {
    const result = await promise;
    console.log(result);
  } catch (error) {
    console.error(error);
  }
}

3. Triển khai trong JavaScript

3.1 Promise Chaining

function fetchUserData(userId) {
  return fetch(`/api/users/${userId}`)
    .then((response) => response.json())
    .then((user) => fetch(`/api/posts?userId=${user.id}`))
    .then((response) => response.json())
    .then((posts) => {
      return {
        user,
        posts,
      };
    })
    .catch((error) => {
      console.error("Error:", error);
      throw error;
    });
}

// Sử dụng
fetchUserData(1)
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

3.2 Promise Combinators

// Promise.all - chờ tất cả promises hoàn thành
const promises = [
  fetch("/api/users"),
  fetch("/api/posts"),
  fetch("/api/comments"),
];

Promise.all(promises)
  .then((responses) => Promise.all(responses.map((r) => r.json())))
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

// Promise.race - lấy kết quả của promise hoàn thành đầu tiên
const timeoutPromise = new Promise((_, reject) =>
  setTimeout(() => reject(new Error("Timeout")), 5000)
);

Promise.race([fetch("/api/data"), timeoutPromise])
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

// Promise.allSettled - chờ tất cả promises kết thúc (thành công hoặc thất bại)
Promise.allSettled(promises).then((results) => {
  results.forEach((result) => {
    if (result.status === "fulfilled") {
      console.log("Success:", result.value);
    } else {
      console.log("Error:", result.reason);
    }
  });
});

4. Triển khai trong TypeScript

4.1 Type-safe Promises

interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json();
}

async function fetchUserPosts(userId: number): Promise<Post[]> {
  const response = await fetch(`/api/posts?userId=${userId}`);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json();
}

// Generic Promise utility types
type AsyncResult<T> = Promise<
  | {
      data: T;
      error: null;
    }
  | {
      data: null;
      error: Error;
    }
>;

async function safeAsync<T>(promise: Promise<T>): AsyncResult<T> {
  try {
    const data = await promise;
    return { data, error: null };
  } catch (error) {
    return {
      data: null,
      error: error instanceof Error ? error : new Error(String(error)),
    };
  }
}

4.2 Custom Promise Implementations

class AsyncQueue<T> {
  private queue: Promise<T>[];
  private concurrency: number;
  private running: number;

  constructor(concurrency: number = 1) {
    this.queue = [];
    this.concurrency = concurrency;
    this.running = 0;
  }

  async add<R>(task: () => Promise<R>): Promise<R> {
    while (this.running >= this.concurrency) {
      await Promise.race(this.queue);
    }

    this.running++;
    const promise = task().finally(() => {
      this.running--;
      this.queue = this.queue.filter((p) => p !== promise);
    });

    this.queue.push(promise);
    return promise;
  }
}

// Sử dụng
const queue = new AsyncQueue(2);

async function processTask(id: number): Promise<string> {
  await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));
  return `Task ${id} completed`;
}

async function example() {
  const tasks = Array.from({ length: 5 }, (_, i) => i);
  const results = await Promise.all(
    tasks.map((id) => queue.add(() => processTask(id)))
  );
  console.log(results);
}

5. Ví dụ Thực Tế: API Client với Retry và Cache

interface CacheEntry<T> {
  data: T;
  timestamp: number;
}

class ApiClient {
  private cache: Map<string, CacheEntry<any>>;
  private cacheDuration: number;
  private maxRetries: number;

  constructor(cacheDuration: number = 5 * 60 * 1000, maxRetries: number = 3) {
    this.cache = new Map();
    this.cacheDuration = cacheDuration;
    this.maxRetries = maxRetries;
  }

  private async retryFetch<T>(
    url: string,
    options: RequestInit,
    retries: number = 0
  ): Promise<T> {
    try {
      const response = await fetch(url, options);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    } catch (error) {
      if (retries < this.maxRetries) {
        const delay = Math.pow(2, retries) * 1000;
        await new Promise((resolve) => setTimeout(resolve, delay));
        return this.retryFetch<T>(url, options, retries + 1);
      }
      throw error;
    }
  }

  private isCacheValid<T>(entry: CacheEntry<T>): boolean {
    return Date.now() - entry.timestamp < this.cacheDuration;
  }

  async get<T>(url: string, forceRefresh: boolean = false): Promise<T> {
    const cacheKey = url;
    const cachedData = this.cache.get(cacheKey);

    if (!forceRefresh && cachedData && this.isCacheValid(cachedData)) {
      return cachedData.data;
    }

    const data = await this.retryFetch<T>(url, {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
      },
    });

    this.cache.set(cacheKey, {
      data,
      timestamp: Date.now(),
    });

    return data;
  }

  async post<T>(url: string, body: any): Promise<T> {
    return this.retryFetch<T>(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });
  }
}

// Sử dụng
interface User {
  id: number;
  name: string;
}

const api = new ApiClient();

async function example() {
  try {
    // Lấy dữ liệu với cache
    const users = await api.get<User[]>("/api/users");
    console.log(users);

    // Force refresh cache
    const freshUsers = await api.get<User[]>("/api/users", true);
    console.log(freshUsers);

    // Post dữ liệu mới
    const newUser = await api.post<User>("/api/users", {
      name: "John Doe",
    });
    console.log(newUser);
  } catch (error) {
    console.error("API Error:", error);
  }
}

5.5 API hiện đại cho Promise (ES2021+)

5.5.1 Promise.any, lấy promise đầu tiên thành công

Promise.any (ES2021) fulfill ngay khi một promise trong mảng fulfill; chỉ reject khi tất cả đều reject (với AggregateError chứa mảng errors). Hữu ích cho racing nhiều mirror:

const mirrors = [
  "https://cdn-us.example.com/data.json",
  "https://cdn-eu.example.com/data.json",
  "https://cdn-ap.example.com/data.json",
];

try {
  const res = await Promise.any(mirrors.map((url) => fetch(url)));
  console.log("Fastest mirror:", res.url);
} catch (err) {
  if (err instanceof AggregateError) {
    console.error("All mirrors failed:", err.errors);
  }
}
APIĐiều kiện fulfillĐiều kiện reject
Promise.allMọi promise fulfillBất kỳ reject → reject ngay
Promise.allSettledMọi promise settle (không bao giờ reject),
Promise.racePromise đầu tiên settle (fulfill hoặc reject)Promise đầu tiên reject
Promise.anyPromise đầu tiên fulfillMọi promise reject → AggregateError

5.5.2 Promise.withResolvers, ES2024

Trước ES2024, muốn expose resolve/reject ra ngoài closure phải viết dài dòng. Promise.withResolvers() (Chrome 119+, Firefox 121+, Node 22+) giải quyết:

// ❌ Cách cũ, verbose
function deferred<T>() {
  let resolve!: (v: T) => void;
  let reject!: (e: unknown) => void;
  const promise = new Promise<T>((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

// ✅ Cách mới, ES2024
const { promise, resolve, reject } = Promise.withResolvers<string>();

// Ví dụ: đóng gói WebSocket connection thành Promise
function connect(url: string): Promise<WebSocket> {
  const { promise, resolve, reject } = Promise.withResolvers<WebSocket>();
  const ws = new WebSocket(url);
  ws.addEventListener("open", () => resolve(ws), { once: true });
  ws.addEventListener(
    "error",
    (e) => reject(new Error("WS failed", { cause: e })),
    { once: true }
  );
  return promise;
}

5.5.3 AbortController + AbortSignal, huỷ async

Mọi Web API hỗ trợ cancellation hiện đại đều nhận AbortSignal: fetch, addEventListener, setTimeout (Node 18+), scheduler.postTask, streams… Không cần viết custom cancel token.

const controller = new AbortController();
const { signal } = controller;

// fetch tự huỷ khi signal abort
const res = await fetch("/api/slow", { signal });

// Huỷ từ bên ngoài
setTimeout(() => controller.abort(new Error("user navigated away")), 5000);

5.5.4 AbortSignal.timeout(), ES2023

// Huỷ sau 5s, không cần tạo controller thủ công
const res = await fetch("/api/data", { signal: AbortSignal.timeout(5000) });

5.5.5 AbortSignal.any([...]), ES2024

Kết hợp nhiều signal → signal mới abort khi bất kỳ signal nào abort. Rất hợp khi cần vừa cho user huỷ vừa timeout:

async function fetchWithBudget(url: string, userSignal?: AbortSignal) {
  const signal = AbortSignal.any(
    [userSignal, AbortSignal.timeout(8000)].filter(Boolean) as AbortSignal[]
  );
  return fetch(url, { signal });
}

5.5.6 Error.cause, chain errors (ES2022)

Thay vì lose context, gắn lỗi gốc:

try {
  await fetchWithBudget("/api/users");
} catch (err) {
  throw new Error("Failed to load users page", { cause: err });
}

Logger hiện đại (pino, winston, browser console) in cả cause thành stack chain. Debugging tăng hiệu suất đáng kể so với re-throw trần.

6. Ưu điểm và Nhược điểm

6.1 Ưu điểm

  • Dễ đọc: Code bất đồng bộ trở nên dễ đọc và maintain hơn
  • Error handling: Xử lý lỗi tập trung và nhất quán
  • Composability: Dễ dàng kết hợp nhiều tác vụ bất đồng bộ
  • Type safety: TypeScript hỗ trợ tốt cho Promises và Async/Await

6.2 Nhược điểm

  • Memory: Promises có thể giữ tài nguyên lâu hơn cần thiết
  • Complexity: Có thể phức tạp khi xử lý nhiều promises đồng thời
  • Error propagation: Lỗi có thể bị “nuốt” nếu không xử lý cẩn thận
  • Performance: Overhead nhỏ so với callbacks thuần túy

7. Khi nào nên sử dụng?

Promises và Async/Await phù hợp khi:

  • Xử lý các tác vụ bất đồng bộ như API calls
  • Cần xử lý lỗi một cách nhất quán
  • Muốn code dễ đọc và maintain
  • Làm việc với nhiều tác vụ bất đồng bộ đồng thời

8. Kết luận

Promises và Async/Await là hai mẫu thiết kế cơ bản và quan trọng trong JavaScript / TypeScript hiện đại. Chúng giúp xử lý code bất đồng bộ một cách dễ dàng và hiệu quả, đồng thời cung cấp cách xử lý lỗi nhất quán.