Async function-Await 函式
async function 可以用來定義一個非同步函式,讓這個函式本體是屬於非同步,但其內部以“同步的方式運行非同步”程式碼。
await 則是可以暫停非同步函式的運行(中止 Promise 的運行),直到非同步進入 resolve 或 reject,當接收完回傳值後繼續非同步函式的運行。
async function getData() {
const data1 = await promiseFn(1); // 因為 await,promise 函式被中止直到回傳
const data2 = await promiseFn(2);
console.log(data1, data2); // 1, 成功 2, 成功
}
getData();
async/await 是 Promise 的語法糖
async/await 並不是新的非同步機制,底層仍然是 Promise。它只是讓 Promise 的串接「讀起來像同步程式碼」,本質完全等價。
// .then() 鏈式寫法
function getUser() {
return fetch('/api/user')
.then(res => res.json())
.then(user => fetch(`/api/posts/${user.id}`))
.then(res => res.json());
}
// async/await 寫法(完全等價,但更易讀)
async function getUser() {
const res = await fetch('/api/user');
const user = await res.json();
const postsRes = await fetch(`/api/posts/${user.id}`);
return postsRes.json();
}
兩個關鍵等價關係:
| async/await | 等價的 Promise 寫法 |
|---|---|
async function f() 永遠回傳 Promise | function f() { return Promise.resolve(...) } |
return x | Promise.resolve(x)(會 unwrap,回傳的若是 Promise 不會包兩層) |
throw err | Promise.reject(err) |
const v = await p | p.then(v => ...) |
面試重點「async/await 比 .then() 好在哪?」——可讀性(避免 callback / .then() 巢狀地獄)、可以用熟悉的 try/catch 與 for/if 控制流程、debug 時 stack trace 更清楚。功能上沒有任何 .then() 做不到而 await 做得到的事。
await 的行為
await p 會做三件事:
- 暫停目前 async 函式的執行(但不會阻塞主執行緒,控制權交還給 event loop,其他程式碼照跑)。
- 等
p進入 settled 狀態:resolve→await回傳 resolved 的值;reject→ 在await那一行拋出例外。 - 把後續程式碼當成 microtask 排入佇列,待
p完成後恢復執行。
console.log(1);
async function run() {
console.log(2);
await null; // 即使 await 一個非 Promise,後面也會變成 microtask
console.log(4); // 這行被排進 microtask queue
}
run();
console.log(3);
// 輸出順序:1 → 2 → 3 → 4
await 一個非 Promise 值await 任何值都會用 Promise.resolve() 包起來。所以 await 5 是合法的,回傳 5,但仍會讓後續程式碼延後到下一個 microtask。
try/catch 錯誤處理
await 的 Promise 若 reject,會在該行拋出例外,因此可以用同步風格的 try/catch 捕捉。這是 async/await 相對 .then() 的一大優勢。
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`); // fetch 不會因 4xx/5xx reject
const data = await response.json();
return data;
} catch (error) {
console.error('獲取資料失敗:', error);
throw error; // 重新拋出讓上層處理
} finally {
console.log('無論成功失敗都會執行(收尾、關 loading)');
}
}
常見陷阱:
fetch只在網路層失敗才 reject,HTTP 4xx/5xx 仍是 resolve,response.ok要自己檢查。- 沒被
await也沒.catch()的 reject 會變成unhandledRejection,可能讓程式崩潰。 try/catch只能抓被await的錯誤;若await漏了(見下方陷阱),catch抓不到。
也可以用 .catch() 對單一 await 做局部處理,回傳預設值而不中斷整個函式:
const data = await fetchData().catch(() => null); // 失敗就 fallback 成 null
並行(Promise.all / allSettled)vs 序列
多個互不相依的非同步操作不該一個一個等,否則總耗時 = 各項相加。
// ❌ 序列執行:總耗時 = A + B + C(互不相依卻白等)
const a = await fetchA();
const b = await fetchB();
const c = await fetchC();
// ✅ 並行執行:總耗時 ≈ max(A, B, C)
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
注意:上面 ✅ 寫法的關鍵是先呼叫(fetchA() 立刻啟動 Promise),再一起 await。下面這種寫法其實還是序列:
// ❌ 假並行,實際仍是序列
const pa = fetchA(); // 啟動
const a = await pa; // 但這裡就等完了
const pb = fetchB(); // B 等 A 完才啟動
Promise.all vs Promise.allSettled vs Promise.race:
| 方法 | 行為 | 何時用 |
|---|---|---|
Promise.all | 任一 reject → 立刻整體 reject(其他結果丟失) | 全部都要成功才有意義 |
Promise.allSettled | 永不 reject,回傳每項 {status, value/reason} | 允許部分失敗,要全部結果 |
Promise.race | 第一個 settled(不管成敗)就回傳 | timeout、取最快的來源 |
Promise.any | 第一個 fulfilled 就回傳,全失敗才 reject | 多來源備援,要任一成功 |
const results = await Promise.allSettled([fetchA(), fetchB()]);
results.forEach(r => {
if (r.status === 'fulfilled') console.log('成功', r.value);
else console.error('失敗', r.reason);
});
Promise.all 的 fail-fast 陷阱Promise.all 在某項 reject 時會「儘早」reject,但其他 Promise 不會被取消,它們仍在背景跑完。若某項拋錯且沒被處理,可能出現 unhandledRejection。需要容錯時用 allSettled。
常見陷阱
1. 迴圈裡的 await 變成序列
for...of 配 await 是序列執行——每圈都等上一圈完成。若各圈互不相依,這會白白拉長總時間。
// ❌ 序列:100 個請求一個一個跑
for (const id of ids) {
await fetchItem(id);
}
// ✅ 並行:一次發出,一起等
await Promise.all(ids.map(id => fetchItem(id)));
但若刻意要序列(例如每圈依賴前一圈結果、或要限制併發、避免打爆 server),for...of + await 才是對的選擇。並非並行永遠比較好。
2. forEach 裡的 await 完全無效
Array.prototype.forEach 不會等待 callback 回傳的 Promise,async callback 的 await 形同虛設,外層也無從等它完成。
// ❌ forEach 不 await:外層的 console.log 會先跑,且錯誤無法捕捉
ids.forEach(async (id) => {
await fetchItem(id);
});
console.log('完成?其實上面都還沒跑完');
// ✅ 用 for...of(序列)或 Promise.all(並行)
for (const id of ids) await fetchItem(id);
// 或
await Promise.all(ids.map(id => fetchItem(id)));
3. 忘記 await
漏掉 await 時,變數會是 Promise 物件本身而非結果值,且 try/catch 抓不到它的 reject。
async function bad() {
const data = fetchData(); // 漏了 await
console.log(data); // Promise { <pending> },不是資料
console.log(data.name); // undefined
}
// 例外:若只是要 fire-and-forget 不需結果,可以不 await,
// 但務必補 .catch() 避免 unhandledRejection
fetchData().catch(console.error);
4. 頂層 await(top-level await)
ES2022 起,ES Module 的最頂層可以直接 await,不必包在 async 函式裡。
// ✅ 僅限 ESM(.mjs 或 package.json "type": "module")
const config = await fetch('/config.json').then(r => r.json());
要點:
- 只在 ES Module 有效,CommonJS(
require)不支援,一般<script>也不行。 - 頂層 await 會阻塞該模組的載入,連帶讓 import 它的模組也等待——用不好會拖慢啟動。
- 在非 module 環境寫頂層
await會直接SyntaxError。
.then() 對比小結
| 面向 | async/await | .then() |
|---|---|---|
| 可讀性 | 像同步程式碼,扁平 | 易陷入鏈式 / 巢狀 |
| 錯誤處理 | try/catch(同步風格) | .catch() |
| 控制流程(if/for) | 自然使用原生語法 | 需在 callback 內處理,較繞 |
| 並行 | 仍需搭配 Promise.all | Promise.all |
| 本質 | 語法糖,底層就是 Promise | 原生 Promise API |
兩者可混用async/await 與 .then() 不互斥。常見組合:用 await 寫主流程,對單一 Promise 用 .catch() 設 fallback(如 await fn().catch(() => default))。
注意事項
await只能在async函式內使用(或 ESM 最頂層await,ES2022+)。async函式永遠回傳Promise,return x會被包成Promise.resolve(x)。- 迭代器(
forEach)中的await不會等待,應改用for...of(序列)或Promise.all(並行)。