← JavaScript Learning Path

JAVASCRIPT V2 · ADVANCED · 132

Advanced Async:單一 Request 都正確,為什麼 UI 還是會錯?

42 與 26 解決單條 Promise pipeline 與 scheduling。真實 UI 的下一層問題是 concurrency:多個 request 同時存在、完成順序不固定、舊 response 晚到、使用者已經不需要原工作。這時 correctness 需要 cancellation 或 version policy。

Learning outcomes

1. Why now:錯誤不一定來自失敗 Request

User types:
j

request A starts

User types:
javascript

request B starts

B finishes first
→ render "javascript"

A finishes later
→ render "j"  // stale overwrite

A、B 都可能 HTTP 200;bug 是 completion order 與 user intent 不一致。

2. Mental model:Start Order ≠ Finish Order

start:
A ────────────────┐
B ───────┐        │
         ↓        ↓
finish:
         B        A

任何依賴「先發出的 request 一定先回」的 UI 都有 race 風險。

3. Worked Example A:Sequential Await

const user =
  await loadUser();

const courses =
  await loadCourses(
    user.id
  );

第二個工作依賴 user.id,因此 sequential 是正確 dependency,而不是效能問題。

4. Worked Example B:Independent Work Concurrent Start

const userPromise =
  loadUser();

const settingsPromise =
  loadSettings();

const [user, settings] =
  await Promise.all([
    userPromise,
    settingsPromise
  ]);

兩個工作沒有 dependency,可以同時 start,再一起 await。

5. Promise.all 的 Contract

await Promise.all([
  loadA(),
  loadB(),
  loadC()
]);

任一 input reject,aggregate Promise reject。但其他工作可能已經開始,Promise.all 不會自動取消它們。

6. allSettled:需要每個結果時

const results =
  await Promise.allSettled([
    loadProfile(),
    loadWeather(),
    loadNews()
  ]);

for (const result of results) {
  console.log(
    result.status
  );
}

如果產品允許 widget 部分成功,allSettled 可能比 fail-fast all 更符合 contract。

7. Worked Example C:AbortController

let controller = null;

async function search(
  query
) {
  controller?.abort();

  controller =
    new AbortController();

  const response =
    await fetch(
      "/api/search?q="
      + encodeURIComponent(
          query
        ),
      {
        signal:
          controller.signal
      }
    );

  return response.json();
}

取消舊 request 可以節省資源,也能避免部分 stale work。但你仍要正確處理 AbortError。

8. Cancellation 不等於所有 async 工作都真的停止

controller.abort();

AbortSignal 只會影響支援它的 API;你自己啟動的 arbitrary Promise/CPU work 不會因為有 controller 就自動中止。Cancellation 是 cooperative contract。

9. Latest-Wins Version Guard

let version = 0;

async function search(
  query
) {
  const mine =
    ++version;

  const result =
    await load(query);

  if (mine !== version) {
    return "stale";
  }

  render(result);
  return "rendered";
}

這種 policy 不一定取消舊工作,但能阻止舊結果覆蓋新 state。

10. Execution trace:Race 被 Version 擋住

A starts
mine = 1

B starts
mine = 2
global version = 2

B finishes
2 === 2
→ render B

A finishes
1 !== 2
→ stale, ignore

11. Common mistakes

A. 只在 Loading state 禁用 UI 就以為沒有 race

產品可能允許快速切換,或 request 在其他入口重入。

B. AbortError 當真正系統錯誤顯示紅色警告

使用者主動換 query 造成的 abort 通常是預期控制流程。

C. Promise.all 當 cancellation

aggregate reject 不會神奇停止其他 operations。

D. 所有 request 都用 latest-wins

付款、寫入等 domain 可能需要 idempotency/serialization,而不是忽略舊結果。

12. Debug evidence:Record Request Identity

console.log({
  requestId,
  query,
  startedAt,
  finishedAt,
  currentVersion
});

遇到 UI「偶爾跳回舊資料」,Network timing + request id + render log 往往比只看最後 state 更有證據力。

13. Guided exercise:makeLatestOnly

function makeLatestOnly() {
  let version = 0;

  return async function (
    load
  ) {
    // TODO
  };
}

每次呼叫取得自己的 version;只有最新版回傳實際 value,其餘回 "stale"。

14. Independent exercise:Cancelable Search

建立 search controller:每次新 query abort 前一次;HTTP 非 2xx 要 throw;AbortError 不顯示 error UI;真正 network/HTTP failure 才顯示。

try {
  // request
} catch (error) {
  if (
    error.name
    === "AbortError"
  ) {
    return;
  }

  throw error;
}

15. Challenge:Read Race 與 Write Race 不一樣

搜尋結果 stale 可以「忽略舊結果」。如果是「扣款 request timeout 後 retry」,不能靠 latest-wins;你需要思考 server-side idempotency key。說明為什麼 read UI race policy 不能直接套到 money-changing write。

Project checkpoint:Grade Tracker Remote Search

input event
  ↓
new query version
  ↓
abort old fetch
  ↓
start new fetch
  ↓
response
  ↓
still latest?
  ├─ no → ignore
  └─ yes → state → render

這把 Stage 4 的 Promise/Event Loop 與 Stage 5 的 closure/version state 串成真正 UI concurrency model。

16. Stage 5 串起來

Function as value
  ↓
Closure keeps environment
  ↓
Identity shapes state
  ↓
RegExp validates text boundary
  ↓
Date/Intl separates domain/display
  ↓
this/prototype explains object behavior
  ↓
immutability clarifies transitions
  ↓
advanced async protects concurrent state

17. Vocabulary / Summary

18. Further reading

Knowledge check · Mastery

  1. 區分 sequential dependency 與 concurrent independent work。
  2. 解釋 stale response race 的完整 timeline。
  3. 完成 latest-only 與 cancelable search。
  4. 完成頁面下方 Executable JS timing tests。