本文 AI 產出,尚未審核

JavaScript 課程:ES6+ 新特性(Modern JS)

主題:Promise 與 async‑await


簡介

在瀏覽器與 Node.js 的非同步環境中,非同步程式碼的可讀性與錯誤處理一直是開發者面臨的挑戰。
ES6(ECMAScript 2015)引入了 Promise,讓我們可以以「承諾」的方式描述尚未完成的工作;而在 ES2017(ES8)中,async / await 進一步把 Promise 包裝成看似同步的語法,大幅提升程式的可維護性。

掌握這兩個概念,等於拿到了解決 I/O、網路請求、計時器、檔案讀寫 等常見非同步任務的金鑰。本文將從概念說明、實作範例、常見陷阱到最佳實踐,完整帶你走進現代 JavaScript 的非同步世界。


核心概念

1. Promise 基礎

Promise 是一個 代表未來結果(成功或失敗)的物件。它有三個狀態:

狀態 說明
pending 初始狀態,尚未完成
fulfilled 成功完成,會傳遞 value
rejected 失敗,會傳遞 reason(錯誤)
// 建立一個 Promise,模擬 2 秒後成功返回資料
const fetchData = new Promise((resolve, reject) => {
  setTimeout(() => {
    const data = { id: 1, name: 'Alice' };
    resolve(data);               // 成功時呼叫 resolve
    // reject(new Error('Network error')); // 失敗時呼叫 reject
  }, 2000);
});

1.1. 使用 .then().catch()

fetchData
  .then(result => {
    console.log('取得資料:', result);
  })
  .catch(err => {
    console.error('發生錯誤:', err);
  });
  • .then() 只在 fulfilled 時執行。
  • .catch() 只在 rejected 時執行,等同於 .then(null, onRejected)

1.2. Promise 鏈(Chain)

// 先取得使用者資料,再根據 id 取得詳細資訊
fetchUser()
  .then(user => fetchProfile(user.id))   // 回傳另一個 Promise
  .then(profile => {
    console.log('使用者個人檔案:', profile);
  })
  .catch(err => console.error(err));

重點:只要回傳的是 Promise.then() 會自動等待它完成,形成「串接」的效應。


2. async / await

async 函式會自動回傳一個 Promise;在 async 函式內使用 await,可以 暫停執行 直到 Promise 完成,語法看起來像同步程式碼。

// async 函式範例
async function getUserInfo() {
  try {
    const user = await fetchUser();           // 等待 fetchUser 完成
    const profile = await fetchProfile(user.id);
    console.log('使用者資訊:', { user, profile });
  } catch (error) {
    console.error('取得資訊失敗:', error);
  }
}
  • await 只能在 async 函式或最外層的模組中使用(ES2022 以上支援 top‑level await)。
  • await拋出 Promise 被 reject 時的錯誤,故必須搭配 try...catch 處理。

3. 重要的 Promise 方法

方法 說明
Promise.all(iterable) 等待所有 Promise 完成,若任一失敗則整體失敗。
Promise.race(iterable) 只要有一個 Promise 完成(成功或失敗)就返回結果。
Promise.allSettled(iterable) 等所有 Promise 完成(不管成功或失敗),返回每個的狀態。
Promise.resolve(value) 把任意值轉成已完成的 Promise。
Promise.reject(reason) 產生已失敗的 Promise。

3.1. Promise.all 範例:同時發送多筆 API

async function loadDashboard() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ]);

  console.log('儀表板資料:', { user, posts, comments });
}

3.2. Promise.race 範例:設定超時機制

function fetchWithTimeout(url, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Request timed out')), ms)
  );

  return Promise.race([fetch(url), timeout]);
}

4. 程式碼範例彙總

以下提供 5 個實用範例,說明 Promise 與 async‑await 在日常開發中的使用方式。

範例 1️⃣:簡易的「延遲」函式(Delay)

// 回傳一個會在指定毫秒後 resolve 的 Promise
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 使用 async/await
async function demoDelay() {
  console.log('開始等待...');
  await delay(1500);               // 暫停 1.5 秒
  console.log('等待結束!');
}
demoDelay();

說明delay 常用於模擬網路延遲、節流(throttling)或測試。


範例 2️⃣:串接多筆 API(錯誤傳遞)

async function fetchAllData() {
  try {
    const user = await fetchUser();          // 失敗會直接跳到 catch
    const orders = await fetchOrders(user.id);
    const details = await Promise.all(
      orders.map(o => fetchOrderDetail(o.id))
    );
    return { user, orders, details };
  } catch (err) {
    console.error('取得資料時發生錯誤:', err);
    throw err; // 讓呼叫端也能感知錯誤
  }
}

範例 3️⃣:使用 Promise.allSettled 處理部分失敗

async function fetchMultipleResources(urls) {
  const fetchPromises = urls.map(url => fetch(url).then(res => res.json()));
  const results = await Promise.allSettled(fetchPromises);

  results.forEach((result, idx) => {
    if (result.status === 'fulfilled') {
      console.log(`第 ${idx + 1} 筆成功:`, result.value);
    } else {
      console.warn(`第 ${idx + 1} 筆失敗:`, result.reason);
    }
  });
}

情境:當我們需要一次取得多筆資料,但不希望單筆失敗就打斷整個流程時,allSettled 非常適合。


範例 4️⃣:實作「重試」機制(Retry)

async function retry(fn, retries = 3, delayMs = 500) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn();          // 成功直接回傳
    } catch (err) {
      if (i === retries - 1) throw err; // 最後一次仍失敗則拋出
      console.warn(`第 ${i + 1} 次嘗試失敗,${delayMs}ms 後重試`);
      await delay(delayMs);
    }
  }
}

// 使用範例:對不穩定的 API 重試
retry(() => fetchUnstableAPI(), 5, 1000)
  .then(data => console.log('最終取得資料:', data))
  .catch(err => console.error('全部重試失敗:', err));

範例 5️⃣:Top‑Level await(ES2022+)在 Node.js

// file: main.mjs
import { fetchUser, fetchPosts } from './api.mjs';

try {
  const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
  console.log('使用者與貼文:', { user, posts });
} catch (e) {
  console.error('初始化失敗:', e);
}

提示:在支援的環境(Node 14+、modern browsers)中,可直接在模組最外層使用 await,省去包一層 async 函式。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記 return .then() 中未回傳 Promise,導致後續 .then() 立即執行。 確保每個 .then() 回傳 Promise(或值)。
過度嵌套 async async 函式內再次使用 new Promise 包裝已是 Promise 的操作,產生「Promise 包巢」 直接回傳原始 Promise,或使用 await
未捕獲錯誤 await 的錯誤未被 try...catch 包住,導致未處理的例外。 在每個 await 前使用 try...catch,或在呼叫端使用 .catch()
Promise.all 的單點失敗 任一子 Promise 失敗會讓整個 all 失敗。 若需要容錯,改用 Promise.allSettled 或自行包裝每個 Promise 為「永遠成功」的結果。
忘記 await 呼叫 async 函式卻忘記 await,導致返回的是未解決的 Promise。 在需要結果的地方加上 await,或使用 .then() 處理。
阻塞主執行緒 使用大量同步迴圈或阻塞 I/O,仍會影響 UI。 把重 CPU 工作交給 Web Workers 或 Node 的 Worker Threads。

最佳實踐清單

  1. 只在需要的地方使用 await:過度 await 會讓程式序列化,失去並行的好處。
  2. 利用 Promise.all 進行批次併發:確保所有請求同時發送,提升效能。
  3. 統一錯誤處理:在最外層建立全域的 process.on('unhandledRejection')(Node)或 window.addEventListener('unhandledrejection')(瀏覽器),避免遺漏。
  4. 避免「回調地獄」:把相關的非同步流程抽成獨立的 async 函式或工具函式,保持程式碼可讀。
  5. 寫測試:使用 Jest、Mocha 等測試框架的 async/await 支援,確保非同步邏輯的正確性。

實際應用場景

場景 典型需求 推薦使用方式
前端資料載入 多個 API 同時取得使用者、商品、推薦列表 Promise.all + await 渲染 UI 前一次性取得所有資料
表單送出 需要先驗證、再上傳檔案、最後寫入資料庫 await 驗證函式 → await 檔案上傳 → await DB 寫入
Node.js 後端 串接第三方服務(OAuth、支付、郵件) async 中使用 try...catch 包住每一步,確保失敗回傳適當的 HTTP 錯誤碼
批次工作(Cron) 每天凌晨抓取多個資料源、合併後寫入報表 Promise.allSettled 處理部分失敗,最後統一產生報表
即時聊天室 需要同時監聽多個 WebSocket、API 呼叫與本地快取 使用 Promise.race 監測「超時」或「最先回應」的情況,提升使用者體驗

範例:在 React 中使用 useEffect 搭配 async 函式載入資料

import { useEffect, useState } from 'react';

function Dashboard() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true; // 防止 component unmount 後 setState

    async function load() {
      try {
        const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
        if (isMounted) setData({ user, posts });
      } catch (e) {
        if (isMounted) setError(e);
      }
    }

    load();

    return () => { isMounted = false; };
  }, []);

  if (error) return <div>發生錯誤:{error.message}</div>;
  if (!data) return <div>載入中…</div>;
  return (
    <div>
      <h1>{data.user.name} 的儀表板</h1>
      {/* 渲染 posts */}
    </div>
  );
}

總結

  • Promise 為非同步流程提供了 可鏈結、可組合 的基礎結構。
  • async / await 把 Promise 包裝成 類同步 的寫法,讓程式碼更易讀、錯誤處理更直觀。
  • 熟練 Promise.allPromise.racePromise.allSettled 等工具,能根據不同需求選擇 併發競賽容錯 的策略。
  • 注意常見陷阱(未返回 Promise、錯誤未捕獲、過度序列化),並遵守最佳實踐(最小化 await、統一錯誤處理、寫測試),即可在 前端、後端或全端 的專案中安全、有效地使用非同步程式碼。

掌握了 Promise 與 async‑await,你就能自信地面對任何網路請求、檔案 I/O 或計時操作,寫出 乾淨、可維護、效能佳 的 JavaScript 程式。祝你在 Modern JS 的旅程中玩得開心、寫得順利! 🚀