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。 |
最佳實踐清單
- 只在需要的地方使用
await:過度await會讓程式序列化,失去並行的好處。 - 利用
Promise.all進行批次併發:確保所有請求同時發送,提升效能。 - 統一錯誤處理:在最外層建立全域的
process.on('unhandledRejection')(Node)或window.addEventListener('unhandledrejection')(瀏覽器),避免遺漏。 - 避免「回調地獄」:把相關的非同步流程抽成獨立的
async函式或工具函式,保持程式碼可讀。 - 寫測試:使用 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.all、Promise.race、Promise.allSettled等工具,能根據不同需求選擇 併發、競賽 或 容錯 的策略。 - 注意常見陷阱(未返回 Promise、錯誤未捕獲、過度序列化),並遵守最佳實踐(最小化
await、統一錯誤處理、寫測試),即可在 前端、後端或全端 的專案中安全、有效地使用非同步程式碼。
掌握了 Promise 與 async‑await,你就能自信地面對任何網路請求、檔案 I/O 或計時操作,寫出 乾淨、可維護、效能佳 的 JavaScript 程式。祝你在 Modern JS 的旅程中玩得開心、寫得順利! 🚀