本文 AI 產出,尚未審核

Promise 與鏈式呼叫


簡介

在 JavaScript 裡,非同步程式碼的管理一直是開發者面臨的挑戰。從最早的 callback 到現在的 async/awaitPromise 扮演了承上啟下的關鍵角色。它不僅讓非同步流程更具可讀性,也提供了 鏈式呼叫 (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. 鏈式呼叫的原理

每一次呼叫 thencatchfinally 都會 回傳一個新的 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 收不到 catchthrowreturn 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.allasync/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.allPromise.racefinally,以支援併行、競賽與資源清理。
  • 遇到過長的鏈,考慮改寫成 async/await 或拆成小函式,以提升可讀性。

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