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);
  }
}

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.