JavaScript 深度
非同步 JS

Promise vs async/await
非同步 JS 的現代寫法

從 Callback Hell 到 Promise 鏈,再到 async/await,
徹底搞懂現代非同步 JavaScript 的寫法與陷阱

J

Joseph Chen

2026·JavaScript 深度系列·5 min read·1.2k views
Promise
async/await
非同步

一、非同步的起點:Callback 與 Callback Hell

JS 最早的非同步寫法是回呼函式(Callback)——把「完成後要做的事」作為參數傳進去, 讓非同步操作完成後呼叫它。但連續的非同步操作會讓 callback 一層套一層,變成難以維護的「Callback Hell」。

callback-hell.js — 地獄長這樣
// 情境:登入 → 取得使用者資料 → 取得訂單 → 取得商品詳情
login(user, (err, session) => {
  if (err) return handleError(err);

  fetchUser(session.userId, (err, user) => {
    if (err) return handleError(err);

    fetchOrders(user.id, (err, orders) => {
      if (err) return handleError(err);

      fetchProductDetail(orders[0].productId, (err, product) => {
        if (err) return handleError(err);

        console.log(product); // 終於拿到資料了…
        // 往右縮排了 4 層,再多幾個操作就失控了
      });
    });
  });
});
⚠️
Callback Hell 的三大問題:
1. 可讀性差:越來越深的縮排讓程式碼難以閱讀。
2. 錯誤處理重複:每一層都要檢查 err。
3. 難以維護:邏輯順序和視覺順序不一致,修改容易出錯。

二、Promise:承諾「這個值未來會到」

Promise 代表一個尚未完成但最終會完成(或失敗)的操作。 它有三種狀態,且狀態只能單向轉換:

Pending

等待中,初始狀態

resolve(value)

Settled

已 resolve,有結果值

Pending

等待中

reject(error)

Rejected

已失敗,有錯誤

💡
Promise 一旦 settle(resolve 或 reject),狀態就永遠不變。 再次呼叫 resolve 或 reject 都不會有任何效果。

Promise 的基本寫法

promise-basic.js
// 建立 Promise
const fetchData = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve({ data: 'Joseph' }); // 成功,傳入結果值
    } else {
      reject(new Error('Failed')); // 失敗,傳入 Error
    }
  }, 1000);
});

// 消費 Promise
fetchData
  .then((result) => {
    console.log(result.data); // 'Joseph'
    return result.data.toUpperCase(); // 可以在 thenreturn 值,傳遞給下一個 then
  })
  .then((upper) => {
    console.log(upper); // 'JOSEPH'
  })
  .catch((err) => {
    console.error(err); // 統一處理任何 then 中的錯誤
  })
  .finally(() => {
    console.log('無論成功失敗都會執行'); // 清理工作
  });

用 Promise 解決 Callback Hell

promise-chain.js — 鏈式取代巢狀
// 同樣的流程,用 Promise 鏈寫
login(user)
  .then((session) => fetchUser(session.userId))
  .then((user) => fetchOrders(user.id))
  .then((orders) => fetchProductDetail(orders[0].productId))
  .then((product) => {
    console.log(product); // 清晰的線性流程
  })
  .catch((err) => {
    handleError(err); // 一個 catch 處理所有錯誤
  });

Promise 的靜態方法

Promise.all()
全部成功才算成功

並行執行多個 Promise,全部 resolve 才 resolve,任一 reject 就立刻 reject。

Promise.all()
// 並行發出多個請求,等全部完成
const [user, orders, notifications] = await Promise.all([
  fetchUser(id),
  fetchOrders(id),
  fetchNotifications(id),
]);
// 比依序等快:總時間 = 最慢的那個,而非全部加總
Promise.allSettled()
全部完成後回報

等所有 Promise 都 settle(無論成功失敗),回傳每個的狀態和結果。適合「失敗也要繼續」的場景。

Promise.allSettled()
const results = await Promise.allSettled([
  fetch('/api/a'),
  fetch('/api/b'), // 就算這個失敗,也不影響其他的
  fetch('/api/c'),
]);
results.forEach(r => {
  if (r.status === 'fulfilled') console.log(r.value);
  if (r.status === 'rejected') console.log(r.reason);
});
Promise.race()
最快的那個

回傳第一個 settle 的結果,常用於超時控制。

Promise.race()
// 5 秒內沒回應就超時
const timeout = new Promise((_, reject) =>
  setTimeout(() => reject(new Error('Timeout')), 5000)
);

const result = await Promise.race([
  fetch('/api/data'),
  timeout,
]);

三、async/await:讓非同步看起來像同步

async/await 是 ES2017 引入的語法糖, 底層仍是 Promise,但讓非同步程式碼的寫法和讀起來的感覺像同步一樣直觀。

async-await-basic.js
// async 函式永遠回傳一個 Promise
async function loadUser(id) {
  // await 暫停函式,等 Promise resolve,拿到結果
  const user = await fetchUser(id);
  const orders = await fetchOrders(user.id);

  return { user, orders }; // 等同於 Promise.resolve({ user, orders })
}

// 呼叫 async 函式
loadUser(123)
  .then(({ user, orders }) => console.log(user, orders))
  .catch(console.error);

// 或者在另一個 async 函式裡用 await
async function main() {
  try {
    const { user, orders } = await loadUser(123);
    console.log(user, orders);
  } catch (err) {
    console.error(err);
  }
}

async/await vs Promise.then 對照

Promise.then 寫法
function loadData(id) {
  return fetchUser(id)
    .then(user => {
      return fetchOrders(user.id)
        .then(orders => ({
          user,
          orders,
        }));
    })
    .catch(err => {
      console.error(err);
    });
}
async/await 寫法
async function loadData(id) {
  try {
    const user = await fetchUser(id);
    const orders = await fetchOrders(
      user.id
    );
    return { user, orders };
  } catch (err) {
    console.error(err);
  }
}
async/await 讓程式碼的邏輯流程更清晰,特別是有條件分支、迴圈、多個依賴步驟的場景。 但它們本質上等價,選擇哪種看個人偏好和團隊風格。

四、async/await 最常見的陷阱

陷阱 #1

在迴圈裡 await,變成依序執行

for...of 迴圈 + await 是依序執行的—— 等第一個完成才執行第二個,總時間是全部加總。 如果每個任務互相獨立,應該並行執行。

❌ 依序執行(慢)
// 假設每個 fetch 需要 1 秒
// 這樣總共要等 3 秒
async function sequential(ids) {
  const results = [];
  for (const id of ids) {
    const data = await fetch(id); // 等這個完成才繼續
    results.push(data);
  }
  return results;
}
✅ 並行執行(快)
// 同時發出三個請求,等最慢的那個
// 總共只要 1 秒(最慢的那個)
async function parallel(ids) {
  const promises = ids.map(id => fetch(id));
  const results = await Promise.all(promises);
  return results;
}

獨立的非同步任務應該用 Promise.all 並行執行。只有當後面的任務依賴前面的結果時,才需要 await 依序執行。

陷阱 #2

忘記 await 導致拿到 Promise 物件而非值

❌ 忘記 await
async function getUser() {
  return fetchUser(1); // Promise,不是 User
}

async function main() {
  const user = getUser(); // 又忘了 await
  console.log(user.name); // undefined!
  // user 是 Promise 物件,不是 user 資料
}
✅ 加上 await
async function getUser() {
  return await fetchUser(1); // User 物件
}

async function main() {
  const user = await getUser(); // 等待解析
  console.log(user.name); // 'Joseph' ✅
}

async 函式的回傳值是 Promise,呼叫時也需要 await。一個常見 bug 是呼叫 async 函式忘記 await,拿到的是 Promise 物件而不是實際的值。

陷阱 #3

未處理的 Promise rejection

如果 Promise reject 了卻沒有 .catch()try/catch, 在 Node.js 中可能導致程式崩潰(現代版本),在瀏覽器中會產生 UnhandledPromiseRejection 警告。

❌ 未處理的 rejection
// 如果 fetchData reject,沒有任何處理
async function main() {
  const data = await fetchData(); // 可能 throw
  console.log(data);
}
main(); // UnhandledPromiseRejection!
✅ 妥善處理錯誤
async function main() {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (err) {
    console.error('Error:', err.message);
    // 可以 fallback、retry 或顯示錯誤訊息
  }
}
main();

每個 async 函式的呼叫處都應該有 try/catch,或者在呼叫時加 .catch()。Promise.all 只要有一個 reject 就整個失敗,考慮用 Promise.allSettled 代替。

陷阱 #4

async 函式不能用在 forEach

Array.forEach 不會等待 async callback, 所有 callback 會幾乎同時啟動,但 forEach 本身不等待它們完成就結束了。

❌ forEach + async(不等待)
async function processAll(items) {
  // forEach 不等待 async callback!
  items.forEach(async (item) => {
    await processItem(item);
  });
  // 這行在所有 processItem 完成前就執行了
  console.log('全部完成');
}
✅ for...of 或 Promise.all
// 依序執行:for...of + await
async function processAll(items) {
  for (const item of items) {
    await processItem(item);
  }
  console.log('全部完成');
}

// 並行執行:Promise.all
async function processAllParallel(items) {
  await Promise.all(items.map(processItem));
  console.log('全部完成');
}

forEach 不支援非同步等待。需要依序處理就用 for...of,需要並行處理就用 Promise.all + .map()。

五、核心重點整理

Callback Hell

深層巢狀讓程式碼難以閱讀和維護,是 Promise 出現的動機

Promise 狀態

Pending → Fulfilled 或 Rejected,狀態不可逆

.then() 鏈

每個 then 回傳新 Promise,可以傳遞值;.catch() 統一處理錯誤

Promise.all

並行執行,全部成功才成功;任一失敗就失敗

Promise.allSettled

並行執行,等全部 settle,不管成敗都繼續

async 函式

永遠回傳 Promise,函式體內可以用 await

await

暫停 async 函式,等 Promise resolve,拿到結果值

forEach + async

不等待,改用 for...of(依序)或 Promise.all(並行)

忘記 await

拿到 Promise 物件而非值,是最常見的 async bug