# Promises: Giải quyết trạng thái, nối chuỗi và xử lý lỗi (/vi/docs/programming/async/promises)



## Tóm tắt nhanh (TL;DR) [#tóm-tắt-nhanh-tldr]

Promise không phải là tiến trình đang chạy ngầm trong hệ thống—nó chỉ là một &#x2A;*chiếc vé hẹn (claim ticket)** đại diện cho kết quả trong tương lai. Nhầm lẫn tai hại này, kết hợp với việc xem nhẹ cơ chế sinh Promise mới của chuỗi handler, từng tạo nên những sự cố production kinh hoàng: một dịch vụ thanh toán bắt lỗi thẻ từ chối trong `.catch()`, chỉ ghi log rồi kết thúc mà không ném lại lỗi, khiến hàm ngầm trả về `undefined`. Promise downstream lập tức chuyển sang trạng thái Fulfilled, kích hoạt bước tiếp theo và xuất kho gửi hàng cho khách dù giao dịch bị từ chối 100%.

> 💡 &#x2A;*Quy tắc bỏ túi:** Mỗi lệnh `.then()`, `.catch()` hay `.finally()` đều tạo ra một Promise downstream hoàn toàn mới. Số phận của nó do giá trị trả về (`return`) hoặc ngoại lệ (`throw`) trong handler quyết định. Muốn bắt bệnh chuỗi bất đồng bộ, hãy truy vết Promise downstream, tuyệt đối không nhìn vào Promise gốc ban đầu.

* **Promise là chiếc vé hẹn lấy kết quả, không phải tác vụ đang chạy:** Reject hay bỏ rơi Promise không hề tự động hủy bỏ socket mạng, câu truy vấn database hay worker thread đang thực thi dưới nền.
* **Resolved không đồng nghĩa với Fulfilled:** Một Promise được xem là "resolved" khi số phận của nó đã được niêm phong. Nó hoàn toàn có thể ở trạng thái **pending nhưng đã resolved** nếu đang nhận nuôi (adopt) một Promise con chưa settled.
* **Mỗi mắt xích sinh ra một Promise downstream độc lập:** Trả về giá trị thường sẽ làm downstream fulfilled; ném lỗi (`throw`) sẽ làm downstream rejected; trả về một Promise/thenable sẽ kích hoạt cơ chế nhận nuôi (adoption).
* **Lựa chọn đúng combinator theo nghiệp vụ:** `Promise.all()` dừng ngay khi có lỗi (fail-fast); `Promise.allSettled()` quan sát trọn vẹn 100% kết quả; `Promise.any()` lấy thành công đầu tiên; `Promise.race()` lấy kết quả sớm nhất bất kể thành bại.
* **Cạm bẫy chết người (Lỗi bị nuốt trọn - Swallowed Error):** Handler `.catch()` là một trạm khôi phục. Nếu chỉ ghi log mà không chủ động `throw err`, downstream Promise sẽ được giải quyết ở trạng thái **fulfilled** với giá trị `undefined`, âm thầm cho phép các nghiệp vụ nguy hiểm phía sau chạy tiếp như thể không hề có sự cố.

***

## Ba trạng thái của Promise [#ba-trạng-thái-của-promise]

Theo đặc tả ECMAScript, một Promise luôn nằm ở một trong ba trạng thái duy nhất:

1. **`pending`**: Trạng thái ban đầu, kết quả chưa được định đoạt.
2. **`fulfilled`**: Tác vụ hoàn thành thành công, mang theo một giá trị (`value`).
3. **`rejected`**: Tác vụ thất bại, mang theo một lý do từ chối (`reason`).

<TermBox term="Settled">
  Một Promise được gọi là **settled** khi nó đã chuyển sang trạng thái `fulfilled` hoặc `rejected`. Quá trình chuyển sang settled là **bất biến và một chiều** — một khi đã settled, Promise không thể đổi trạng thái hay đổi giá trị lần thứ hai.
</TermBox>

<AtlasIllustration id="promise-state-machine" />

```js
const p1 = new Promise((resolve) => {
  resolve(42);
  resolve(100); // Lệnh này hoàn toàn bị bỏ qua, p1 vĩnh viễn là 42
});
```

***

## "Resolved" khác với "Fulfilled" như thế nào? [#resolved-khác-với-fulfilled-như-thế-nào]

Đây là điểm gây nhầm lẫn phổ biến nhất ngay cả với các lập trình viên nhiều năm kinh nghiệm:

<TermBox term="Promise Adoption">
  **Promise Adoption (Nhận nuôi)** xảy ra khi bạn giải quyết một Promise bằng một Promise khác. Lúc này, Promise bên ngoài bị khóa chặt số phận theo Promise bên trong, nhưng bản thân nó vẫn ở trạng thái `pending` cho tới khi Promise bên trong chuyển sang settled.
</TermBox>

```js
const inner = new Promise((resolve) => {
  setTimeout(() => resolve('Xong!'), 1000);
});

const outer = new Promise((resolve) => {
  resolve(inner); // outer đã resolved, nhưng vẫn pending trong 1 giây!
});

console.log(outer); // Promise { <pending> }
```

***

## Cơ chế nối chuỗi (Chaining) và tạo Promise Downstream [#cơ-chế-nối-chuỗi-chaining-và-tạo-promise-downstream]

Khi gọi `.then()`, `.catch()`, hay `.finally()`, JavaScript không biến đổi Promise hiện tại mà luôn cấp phát một đối tượng Promise mới trong bộ nhớ heap:

```text
P0 --then(fn1)--> P1 --catch(fn2)--> P2
```

<AtlasIllustration id="promise-chain-outcomes" />

### Bảng đối chiếu kết quả của Handler [#bảng-đối-chiếu-kết-quả-của-handler]

| Hành động trong Handler                       | Trạng thái Downstream Promise | Giá trị Downstream                  |
| :-------------------------------------------- | :---------------------------- | :---------------------------------- |
| Trả về một giá trị thông thường (`return 42`) | **`fulfilled`**               | `42`                                |
| Ném ra một lỗi (`throw new Error()`)          | **`rejected`**                | Đối tượng `Error`                   |
| Trả về một Promise khác (`return fetch()`)    | **Nhận nuôi (Adopt)**         | Phụ thuộc vào kết quả của `fetch()` |
| Không trả về gì (`undefined`)                 | **`fulfilled`**               | `undefined`                         |

### Tự kiểm tra: chuỗi này settle thành gì? [#tự-kiểm-tra-chuỗi-này-settle-thành-gì]

Dự đoán giá trị fulfillment của `p1` trước khi mở đáp án.

```js
const p0 = Promise.resolve({ id: 7 });
const p1 = p0.then((user) => {
  loadProfile(user.id); // cố ý bỏ return trong bài tập này
  return user.id;
});
```

Giả sử `loadProfile()` trả về một Promise fulfill với đối tượng hồ sơ người dùng.

<details>
  <summary>
    Xem giải thích chi tiết
  </summary>

  * Handler `return user.id` (`7`) nên `p1` **fulfill** với giá trị `7`.
  * `loadProfile(user.id)` vẫn được kích hoạt, nhưng Promise của nó **không** được adopt vì không được `return`.
  * Đoạn mã phía sau `await p1` sẽ tiếp tục chạy với giá trị `7` trong khi hồ sơ người dùng có thể vẫn chưa tải xong—lỗi thứ tự này tạo cảm giác mã "đã bất đồng bộ" nhưng thực chất mắt xích xử lý đã bị đứt gãy hoàn toàn.
</details>

***

## Phòng thí nghiệm Promise (Interactive Lab) [#phòng-thí-nghiệm-promise-interactive-lab]

Sử dụng môi trường mô phỏng dưới đây để từng bước quan sát cách các Promise được khởi tạo, resolve, và lan truyền kết quả:

<PromiseResolutionLab />

***

## Lan truyền lỗi và Khôi phục (Recovery) [#lan-truyền-lỗi-và-khôi-phục-recovery]

Lỗi trong chuỗi Promise hoạt động như một thác nước: nếu mắt xích hiện tại không có handler xử lý lỗi (`onRejected`), lỗi sẽ nhảy cóc qua tất cả các `.then()` trung gian cho đến khi gặp `.catch()` đầu tiên.

<AtlasIllustration id="promise-error-propagation" />

```js
fetchUser(id)
  .then((user) => fetchSettings(user))
  .then((settings) => applyTheme(settings))
  .catch((err) => {
    // Khôi phục bằng giá trị mặc định:
    return defaultSettings;
  })
  .then((settings) => {
    // Bước này VẪN ĐƯỢC CHẠY nếu .catch() phía trên trả về defaultSettings!
    console.log('Áp dụng cài đặt:', settings);
  });
```

### Tự kiểm tra: `.catch()` có chặn handler phía sau không? [#tự-kiểm-tra-catch-có-chặn-handler-phía-sau-không]

```js
fetchCart()
  .catch((error) => {
    report(error);
    return emptyCart();
  })
  .then((cart) => renderCheckout(cart));
```

Nếu `fetchCart()` reject và `emptyCart()` trả về `{ items: [] }`, `renderCheckout` có chạy không?

<details>
  <summary>
    Xem giải thích chi tiết
  </summary>

  * Có. Handler `.catch()` khôi phục bằng cách trả về cart bình thường, nên Promise do `.catch()` trả về **fulfill**.
  * `.then()` tiếp theo vì thế vẫn chạy với cart đã khôi phục.
  * Trường hợp nguy hiểm là `.catch()` chỉ log rồi `return` ngầm `undefined`, khiến bước “thành công” phía sau vẫn chạy.
</details>

***

## Bốn Combinators chuẩn theo mục đích thiết kế [#bốn-combinators-chuẩn-theo-mục-đích-thiết-kế]

<TermBox term="Promise Combinator">
  Một **Promise Combinator** là một phương thức tĩnh nhận vào một mảng/tập hợp các Promise và trả về một Promise duy nhất tổng hợp kết quả của chúng.
</TermBox>

<AtlasIllustration id="promise-combinators" />

| API                    | Nhu cầu sử dụng                                        | Hành vi khi có lỗi (Failure)                                    |
| :--------------------- | :----------------------------------------------------- | :-------------------------------------------------------------- |
| `Promise.all()`        | Cần tất cả input cùng thành công                       | **Fail-fast:** Reject ngay khi bất kỳ input nào reject          |
| `Promise.allSettled()` | Cần kết quả của mọi input (dù thành công hay thất bại) | Luôn fulfill với mảng `{ status, value / reason }`              |
| `Promise.any()`        | Chỉ cần ít nhất một input thành công đầu tiên          | Chỉ reject khi **tất cả** input đều thất bại (`AggregateError`) |
| `Promise.race()`       | Cần phản hồi sớm nhất (thành công hoặc thất bại)       | Settle ngay theo kết quả của Promise về đích đầu tiên           |

***

## Sự cố thực tế cần tránh [#sự-cố-thực-tế-cần-tránh]

### Kịch bản thực tế: "Lỗi bị nuốt trọn" (Swallowed Error) dẫn đến giao hàng miễn phí [#kịch-bản-thực-tế-lỗi-bị-nuốt-trọn-swallowed-error-dẫn-đến-giao-hàng-miễn-phí]

Một hệ thống thương mại điện tử xử lý thanh toán đơn hàng bằng chuỗi Promise như sau:

```ts
function processOrderPayment(orderId: string) {
  return chargeCustomer(orderId)
    .catch((err) => {
      // Lập trình viên chỉ ghi log lỗi trừ thẻ nhưng quên ném lại lỗi (rethrow):
      logger.error('Failed to charge card', { orderId, err });
    })
    .then(() => {
      // Bước này VẪN CHẠY vì .catch() phía trên ngầm trả về undefined (fulfilled)!
      return markOrderAsPaidAndDispatch(orderId);
    });
}
```

* **Hậu quả:** Khi thẻ khách hàng không đủ số dư, `chargeCustomer` bị reject. Tuy nhiên `.catch()` chỉ ghi log rồi kết thúc hàm mà không ném lỗi tiếp (`return undefined`). Điều này biến downstream Promise thành **fulfilled**! Hệ thống lập tức gọi `markOrderAsPaidAndDispatch(orderId)`, xuất kho và gửi hàng cho khách dù chưa thu được đồng nào!
* **Nguyên nhân cốt lõi:** Lập trình viên quên quy tắc cơ bản: `.catch()` là một trạm &#x2A;*khôi phục (recovery)**. Nếu không ném lỗi tiếp, downstream sẽ mặc định hiểu là sự cố đã được khắc phục hoàn toàn.
* **Cách khắc phục chuẩn:** Luôn rethrow lỗi nếu bạn chỉ muốn ghi log hoặc quan sát sự cố:
  ```ts
  return chargeCustomer(orderId)
    .catch((err) => {
      logger.error('Failed to charge card', { orderId, err });
      throw err; // Tiếp tục đẩy lỗi xuống downstream để chặn đơn hàng
    })
    .then(() => markOrderAsPaidAndDispatch(orderId));
  ```

### Kịch bản thực tế: Quên `return` khiến toast báo “đã lưu” quá sớm [#kịch-bản-thực-tế-quên-return-khiến-toast-báo-đã-lưu-quá-sớm]

```js
loadUser()
  .then((user) => {
    saveUser(user); // quên return
  })
  .then(() => {
    showToast('Saved');
  });
```

* **Hậu quả:** Toast có thể hiện khi `saveUser` vẫn đang chạy—hoặc sau khi nó reject—nên sản phẩm báo thành công trước khi biết dữ liệu đã bền vững.
* **Nguyên nhân cốt lõi:** Handler đầu trả về `undefined` ngay, nên Promise downstream fulfill mà không adopt Promise của `saveUser`.
* **Cách khắc phục chuẩn:** `return` đúng công việc bất đồng bộ cần nối chuỗi:

```js
loadUser()
  .then((user) => saveUser(user))
  .then(() => {
    showToast('Saved');
  });
```

***

## Bài tập kiểm tra tư duy [#bài-tập-kiểm-tra-tư-duy]

Hãy dự đoán thứ tự in ra màn hình của đoạn mã sau trước khi mở đáp án:

```js
console.log('1');

Promise.resolve().then(() => {
  console.log('2');
  return Promise.resolve('3');
}).then((val) => {
  console.log(val);
});

Promise.resolve().then(() => {
  console.log('4');
});

console.log('5');
```

<details>
  <summary>
    Xem giải thích chi tiết
  </summary>

  **Thứ tự in ra:** `1 -> 5 -> 2 -> 4 -> 3`

  **Giải thích từng bước:**

  1. Mã đồng bộ chạy trước: In `1`, sau đó in `5`.
  2. Hai microtask đầu tiên được xếp vào hàng đợi:
     * Microtask A (in `2`).
     * Microtask B (in `4`).
  3. Chạy Microtask A: in `2`. Hàm này trả về một `Promise.resolve('3')`. Theo cơ chế Promise Adoption, engine cần thêm các microtask nội bộ để làm phẳng (flatten) Promise này.
  4. Chạy Microtask B: in `4`.
  5. Sau khi Promise con được giải quyết xong, callback downstream tiếp theo mới được đẩy vào microtask queue và in ra `3`.
</details>

***

## Checklist rà soát mã nguồn cho Kỹ sư [#checklist-rà-soát-mã-nguồn-cho-kỹ-sư]

Trước khi đưa mã nguồn sử dụng Promise vào production, hãy đối chiếu với checklist sau:

* [ ] **Xử lý lỗi trọn vẹn:** Mọi nhánh `.catch()` đã chủ động `throw` lại lỗi nếu không có dữ liệu khôi phục hợp lệ chưa?
* [ ] **Tránh lồng nhau vô nghĩa (Promise Hell):** Đã phẳng hóa chuỗi bằng cách `return` Promise thay vì viết lồng `.then()` bên trong `.then()` chưa?
* [ ] **Lựa chọn combinator chính xác:** Đã sử dụng `Promise.allSettled()` cho các tác vụ hàng loạt (batch operations) độc lập chưa?
* [ ] **Quản lý tài nguyên với `finally()`:** Các tác vụ dọn dẹp (tắt loading spinner, đóng kết nối DB) đã được đặt trong `.finally()` để luôn được thực thi chưa?
* [ ] **Không bọc lại Promise thừa thãi:** Tránh viết `new Promise((res, rej) => existingPromise.then(res, rej))`.

***

## Tài liệu quy chuẩn [#tài-liệu-quy-chuẩn]

* [ECMAScript Language Specification (ECMA-262) — Promise Objects](https://tc39.es/ecma262/#sec-promise-objects) — Đặc tả quy chuẩn ngôn ngữ về vòng đời, trạng thái và thuật toán Promise Resolution.
* [MDN Web Docs — Using Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) — Hướng dẫn thực hành chuẩn mực từ Mozilla Developer Network.
