本文 AI 產出,尚未審核

Promise 與鏈式呼叫


簡介

在 JavaScript 裡,非同步程式碼的管理一直是開發者面臨的挑戰。從最早的 callback 到現在的 async/await,Promise 扮演了承上啟下的關鍵角色。它不僅讓非同步流程更具可讀性,也提供了 鏈式呼叫 (chaining) 的機制,讓我們可以把多個非同步步驟依序組合成一條清晰的執行路徑。

掌握 Promise 與鏈式呼叫,意味著能夠:

  • 避免「回呼地獄」,讓程式結構更平坦。
  • 統一錯誤處理,只要在最後一個 catch 就能捕獲整條鏈的錯誤。
  • 靈活組合非同步任務,例如串接 API、併行執行或延遲執行等。

本篇文章將從概念、實作範例、常見陷阱到最佳實踐,完整說明 Promise 與鏈式呼叫的使用方式,適合 初學者到中級開發者 參考。


核心概念

1. Promise 基礎

Promise 是一個代表未來完成或失敗的物件。它有三種狀態:

狀態 說明
pending 尚未完成或失敗,仍在等待中
fulfilled 任務成功完成,會傳遞一個值
rejected 任務失敗,會傳遞一個錯誤原因
// 建立一個簡單的 Promise
const delay = (ms) => {
  return new Promise((resolve, reject) => {
    if (ms < 0) {
      // 直接 reject,示範錯誤情況
      reject(new Error('延遲時間不能為負數'));
    } else {
      setTimeout(() => resolve(`等待了 ${ms} 毫秒`), ms);
    }
  });
};

2. then / catch / finally

  • then(onFulfilled, onRejected):當 Promise fulfilled 時呼叫第一個回呼,若 rejected 且提供第二個回呼則執行之。
  • catch(onRejected):等同於 then(undefined, onRejected),專門處理錯誤。
  • finally(onFinally):不論成功或失敗,都會在最後執行,常用於清理資源。
delay(1000)
  .then(msg => {
    console.log('成功:', msg);   // 成功: 等待了 1000 毫秒
    return '下一步的資料';
  })
  .catch(err => {
    console.error('錯誤:', err);
  })
  .finally(() => {
    console.log('不論結果,都會執行這裡');
  });

3. 鏈式呼叫的原理

每一次呼叫 then、catch 或 finally 都會 回傳一個新的 Promise。這個新 Promise 的狀態取決於回呼函式的回傳值:

回呼回傳值 新 Promise 的結果
普通值 (非 Promise) 立即 fulfilled,值為回傳值
Promise 會 跟隨 該 Promise 的狀態,等同於「扁平化」
拋出錯誤 新 Promise 立即 rejected,錯誤會傳到下一個 catch

因此,我們可以把多個非同步步驟串成一條鏈,每一步只需要關注自己的邏輯,而不必手動管理回呼層級。


程式碼範例

範例 1:基本的 Promise 鏈

// 假設有三個非同步 API,分別回傳字串
const fetchA = () => Promise.resolve('A');
const fetchB = (prev) => Promise.resolve(prev + 'B');
const fetchC = (prev) => Promise.resolve(prev + 'C');

fetchA()
  .then(res => {
    console.log(res);          // A
    return fetchB(res);
  })
  .then(res => {
    console.log(res);          // AB
    return fetchC(res);
  })
  .then(res => {
    console.log('最終結果:', res); // 最終結果: ABC
  })
  .catch(err => console.error(err));

重點:每一次 then 都回傳新的 Promise,使得後續的 then 能取得前一步的結果。

範例 2:錯誤傳遞與捕獲

const riskyTask = (num) => {
  return new Promise((resolve, reject) => {
    if (num % 2 === 0) resolve(num);
    else reject(new Error('奇數不允許'));
  });
};

riskyTask(2)
  .then(v => {
    console.log('第一步成功:', v); // 2
    return riskyTask(v + 1);        // 3 -> 會 reject
  })
  .then(v => console.log('不會到這裡', v))
  .catch(err => {
    console.error('捕獲錯誤:', err.message); // 捕獲錯誤: 奇數不允許
    // 仍然回傳一個值,讓後面的 then 可以繼續執行
    return 0;
  })
  .then(v => console.log('恢復後的值:', v)); // 恢復後的值: 0

技巧:在 catch 中回傳值會把錯誤「轉換」為成功狀態,讓鏈可以繼續。

範例 3:Promise.all 並行執行

const api1 = () => delay(500).then(() => '結果1');
const api2 = () => delay(300).then(() => '結果2');
const api3 = () => delay(700).then(() => '結果3');

Promise.all([api1(), api2(), api3()])
  .then(results => {
    console.log('全部完成:', results);
    // => 全部完成: [ '結果1', '結果2', '結果3' ]
  })
  .catch(err => console.error('其中一個失敗:', err));

說明:Promise.all 會等所有 Promise 都 fulfilled 才 resolve,任一 reject 則立即 reject。

範例 4:把傳統 Callback 轉成 Promise

// 典型的 Node.js 風格 callback
const fs = require('fs');

function readFilePromise(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf8', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

// 使用方式
readFilePromise('./data.txt')
  .then(content => console.log('檔案內容:', content))
  .catch(err => console.error('讀檔失敗:', err));

實務:在舊有程式庫中,將 callback 包裝成 Promise,讓它可以無縫加入鏈式呼叫。

範例 5:結合 async/await 的混合寫法

async function processData() {
  try {
    const a = await fetchA();               // 等同於 Promise
    const b = await fetchB(a);
    const c = await fetchC(b);
    console.log('最終結果 (await):', c);
  } catch (e) {
    console.error('發生錯誤:', e);
  } finally {
    console.log('任務結束');
  }
}
processData();

提示:async/await 本質上仍是 Promise,了解鏈式呼叫的原理能幫助你在兩者之間自由切換。


常見陷阱與最佳實踐

陷阱 為何會發生 解決方式
忘記 return 在 then 內部沒有回傳 Promise,導致下一個 then 收到 undefined Always return the inner Promise or value
錯誤被吞掉 在 then 中自行捕獲錯誤但未重新拋出,會讓外層 catch 收不到 在 catch 後 throw 或 return Promise.reject
過度鏈式 鏈太長難以維護,且每一步都要寫 return 使用 async/await 把邏輯拆成小函式
混用同步例外 在 then 內部直接拋出同步錯誤,若未被捕獲會變成 rejected,但有時不易意識到 用 try...catch 包住同步程式碼,或直接 return Promise.reject(err)
忘記處理未捕獲的 Rejection Node.js 會在未捕獲的 Promise rejection 時拋出警告,甚至終止程式 為所有最外層的 Promise 加上 .catch,或全局監聽 process.on('unhandledRejection')

最佳實踐

  1. 始終返回 Promise:每個 then / catch 都應回傳值或 Promise,讓鏈保持可預測。
  2. 集中錯誤處理:盡量在鏈的最後放一個 catch,或使用 Promise.allSettled 取得每個任務的成功/失敗結果。
  3. 使用 finally 清理資源:如關閉檔案、取消計時器或隱藏 loading UI。
  4. 避免過度嵌套:如果同一層需要多個非同步操作,考慮 Promise.all 或 async/await。
  5. 保持可讀性:適度加入註解,並把長鏈拆成具名函式,例如 fetchUserData()、processOrder()。

實際應用場景

場景 為什麼適合使用 Promise 鏈 範例簡述
前端表單送出 需要依序驗證、上傳檔案、呼叫 API,且每一步若失敗都要中止 validate() → uploadFile() → submitData()
多階段資料處理 從資料庫抓取原始資料 → 轉換格式 → 寫入快取 → 回傳結果 db.get() → transform() → cache.set()
併行下載多個資源 多個下載任務可同時執行,最後一次性處理結果 Promise.all([download1, download2, download3])
輪詢/重試機制 必須在失敗後延遲再重試,且每次重試都返回 Promise retry(() => fetch(url), 3, 2000)
舊有 Callback API 包裝 把第三方庫的 callback 轉成 Promise,統一錯誤處理 上述「readFilePromise」範例

總結

Promise 為 JavaScript 的非同步模型帶來了可組合、可預測的特性,而鏈式呼叫則是將多個非同步步驟像流水線一樣串起來的關鍵技巧。掌握以下要點,即可在實務開發中得心應手:

  • 每個 then / catch 必須回傳(值或 Promise),才能保證鏈的正確傳遞。
  • 集中錯誤處理:最後的 catch 能捕獲整條鏈的例外,減少重複程式碼。
  • 適時使用 Promise.all、Promise.race、finally,以支援併行、競賽與資源清理。
  • 遇到過長的鏈,考慮改寫成 async/await 或拆成小函式,以提升可讀性。

只要熟練這套「Promise + 鏈式呼叫」的思維模式,你就能在前端與後端的非同步開發中,寫出 乾淨、可維護、錯誤可控 的程式碼。祝你寫程式愉快!