一句摘要:await 不會暫停整個 JavaScript;它只暫停目前 async function 中依賴結果的後半段,將控制權交還給呼叫端,並在 Promise settled 後以 microtask 恢復。
前置知識:理解 Promise 會保存成功值或失敗原因,並將後續 reaction 排程執行。
學習路線:從「Promise 表示未來結果」進一步解釋 await 如何交還控制權。
標籤:JavaScript async await Promise Microtask Execution Context Event Loop
await 前後執行、暫停並交還控制權。await 的等待、錯誤傳遞與同步工作仍可能阻塞的邊界。Day 10 將 Promise 看成未來結果的狀態容器。實際開發時,我們更常用 await 取得這個結果。
在 Day 10,我們看到 .then() handler 會以 reaction job 稍後執行;await 可以理解成把同樣的等待關係,寫進 async function 的控制流程裡。
但「等待」很容易讓人誤會:
是 fetch() 停住了?
整個瀏覽器停住了?
還是只有目前 async function 中依賴結果的後半段暫時無法繼續?
答案是最後一個:
await暫停的是目前 async function 中、依賴 await 結果的後續程式;它不會佔住 JavaScript 執行權。

先看最小的例子:
async function prepareScreen() {
console.log('1. function 開始');
await Promise.resolve('設定已讀取');
console.log('3. await 後:更新畫面');
}
prepareScreen();
console.log('2. 呼叫端繼續執行');
輸出順序是:
1. function 開始
2. 呼叫端繼續執行
3. await 後:更新畫面
即使 Promise.resolve() 已經是 fulfilled,await 後面的程式也不會留在目前的 call stack 裡繼續跑。
這說明兩件事:
await。await 後,這個 function 把控制權交還給呼叫端;後半段會在 microtask 中再回來。可以先用這張圖記住:
呼叫 prepareScreen()
│
├─ 印出「function 開始」
├─ 遇到 await
│ └─ 安排「await 後的 continuation」
└─ 將 Promise 回傳給呼叫端
呼叫端繼續執行
└─ 印出「呼叫端繼續執行」
目前同步程式結束後
└─ microtask:回到 prepareScreen(),印出「更新畫面」
所以 async function 不是「呼叫後什麼都晚點再開始」;更準確地說,它會立刻開始,直到遇到第一個 await 才交還控制權。
Await(value) 看:它不把 Promise 回傳給變數看到規範裡的 Await(value),容易產生一個誤解:await 是不是「回傳一個 Promise」?
其實不是。程式從 await 恢復執行後,交給變數的是 Promise 完成後的值,不是 Promise 本身。真正會回傳 Promise 的,是整個 async function 呼叫:
async function getCount() {
const count = await Promise.resolve(42);
// count 是 42,不是 Promise
return count + 1;
}
const result = getCount();
// result 是 Promise;fulfilled 後的值是 43
所以這三種寫法都能使用 await:
await fetch('/api/profile'); // 既有 Promise
await 42; // 先正規化成 fulfilled Promise
await { then(resolve) { resolve('ok'); } }; // thenable
不過使用者不會拿到這個內部 Promise。你在 const value = await ... 取得的是它完成後的值;若 rejected,控制流程則進入 catch。
await 必須搭配 async?這是我一開始學 JavaScript 使用 async 和 await 時,教科書都很強調,是因為他們做的事有不同嗎?
await 不是建立另一條執行緒,而是透過 async function 可暫停、可恢復的執行機制運作,先看看 TC39 的 Await(value) 規範步驟與白話理解對照成這張表:
| TC39 規範步驟 | 白話理解 |
|---|---|
取得目前的 asyncContext |
記住現在執行到哪裡,之後才能從 await 後面繼續。 |
PromiseResolve(%Promise%, value) |
把一般值、Promise 或 thenable 統一轉成可等待的 Promise。 |
建立 fulfilledClosure 與 rejectedClosure |
準備成功與失敗兩條恢復路徑。 |
PerformPromiseThen(promise, onFulfilled, onRejected) |
向 Promise 登記:結果出現後要恢復哪一條流程。 |
移除並暫停 asyncContext,恢復 caller context |
目前 async function 先停在 await,把控制權交還給呼叫端。 |
Promise settled 後恢復 asyncContext |
fulfilled 時讓 await 得到值;rejected 時則像在原地 throw。 |
async 就像先向 JavaScript 宣告:「這個 function 可能在中途暫停,稍後再回來完成。」
解析器因此允許 function body 使用 await;執行引擎也會為這次呼叫準備 Promise 與可暫停、可恢復的執行流程。真正遇到 await 時,才會發生 execution context 的交還與恢復。
因為 async function 可能先交還控制權、未來才完成,呼叫端需要一個東西代表這份尚未完成的工作,就是之前提到的接受未來狀態的容器 Promise 。所以async 所宣告的對外契約,就是讓 function 先回傳 Promise:
async function loadProfile() {
const response = await fetch('/api/profile');
return response.json();
}
const pendingProfile = loadProfile();
// 呼叫當下立刻得到 Promise,而不是尚未存在的 profile
這個 Promise 會保存 async function 「稍後會成功或失敗」的結果:function 的 return 會成為 fulfilled value,未處理的 throw 則會成為 rejection reason。普通 function 沒有這個完成契約,因此不能在 function body 裡直接使用 await:
function loadProfile() {
const response = await fetch('/api/profile');
// SyntaxError:普通 function 沒有 async 的完成契約
return response;
}
這不是「async 讓 fetch 變成非同步」。fetch 本來就回傳 Promise;async 的作用是讓你的 function 可以使用 await 切開控制流程,並將自己最終的 return/throw 統一包成 Promise。
await 暫停的是「依賴結果的後半段」理解上面的 aync context 轉移概念後,把上面的程式拆成兩半,會更容易在程式碼中看出邊界:
先用一個有兩次 await 的範例,看看 loadProfile() 如何暫停與恢復:
async function loadProfile() {
console.log('A. 開始 request');
// 發出 request;等待期間,loadProfile 先暫停
const response = await fetch('/api/profile');
console.log('C. 現在才有 response');
// 等待 response body 讀取並解析完成
const profile = await response.json();
console.log('D. 現在才有 profile', profile.name);
}
console.log('0. 呼叫前');
loadProfile();
console.log('B. loadProfile 已交還控制權');
1. 呼叫 loadProfile(),先印出 A。
2. 遇到第一個 await,loadProfile 暫停,因此呼叫端繼續印出 B。
3. response 抵達後,loadProfile 恢復,印出 C;接著在第二個 await 再暫停一次。
4. JSON 資料準備完成後,loadProfile 再次恢復,最後印出 D。
fetch()沒有啟動另一條 JavaScript 執行流程;等待網路時,是瀏覽器或 Node.js 在處理外部工作。
下一篇 Day 12 會把這個模型延伸到 Async Iterator:每次呼叫 next(),consumer 都等待一個 Promise<IteratorResult>,下一筆就緒後才恢復自己的後續工作。到了 Day 16,Array.fromAsync() 也會運用同樣的概念,逐筆等待並收集資料。
await 會交還控制權,所以多條 async function 不會各自從頭跑到底。先用一個不需要網路的範例觀察:
async function step(name) {
console.log(`${name}: start`);
await null;
console.log(`${name}: end`);
}
step('first');
step('second');
console.log('script end');
輸出順序為:
first: start
second: start
script end
first: end
second: end
await null 不是真的等待網路;它仍會將 null 正規化成一個 fulfilled Promise。不過 await continuation 還是會進 microtask,因此兩次呼叫都先跑到 await,然後才依排入順序恢復。
目前同步程式
├─ step('first') → 排入 first continuation
├─ step('second') → 排入 second continuation
└─ 印出 script end
microtask queue
├─ first continuation
└─ second continuation
這裡不要把「交錯」理解成兩段 JavaScript 同時在主執行緒執行。
await 會將 function 切成多段,後半段以 Promise Job 的形式交給 host 排程;在瀏覽器中,這類 Job 會進入 microtask queue。目前的同步程式結束後,microtask 才會依序被取出,將對應的 execution context 放回 call stack 執行。每一段仍會跑到結束,下一個 microtask 才能開始。
上一節看到,Await(value) 會先用 PromiseResolve 正規化右側的值。因此一般值不需要外部工作才會得到,仍會造成一次非同步切分:
async function example() {
console.log('before');
await 42;
console.log('after');
}
example();
console.log('outside');
輸出是:
before
outside
after
這是很重要的閱讀線索。看到 await 時,不只要問「它在等哪個 API」,也要問「這裡是否真的需要把控制流程切成下一個 microtask?」如果右邊已經是同步可得的值,額外的 await 通常沒有必要,還可能讓 log 順序與錯誤堆疊更難追。
await 如何把 rejection 丟回 try...catch?await 會把 Promise 的 rejection 帶回 async function 的暫停點。
async function loadProfile() {
try {
const response = await fetch('/api/profile');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('profile 載入失敗', error);
throw error;
}
}
當等待中的 Promise rejected 時,async function 的 continuation 會在 microtask 恢復;對 await expression 而言,這次恢復就像在原地 throw rejection reason。因此控制流程直接跳到同一條 async function 裡最近的 catch。
fetch Promise rejected
↓
microtask:恢復 loadProfile()
↓
await 表現得像 throw error
↓
try...catch 接住 error
範例中的 catch 記錄錯誤後再次 throw,所以 loadProfile() 回傳的 Promise 會變成 rejected。若 function 內根本沒有 catch,未被接住的 rejection 也同樣會成為它所回傳 Promise 的失敗原因,交由呼叫端處理:
loadProfile().catch(reportProfileError);
所以 try...catch 的邊界不是「所有非同步錯誤都會自動被全域接住」,而是:這個 try 所在的 async function 是否正在 await 那個會 rejected 的 Promise。
await 不阻塞,長同步工作仍然會卡住「async / await 不會阻塞」很容易被過度簡化。更完整的說法是:
等待 Promise 的期間
→ async function 交還控制權,不阻塞主執行緒
await 前、await 後每一段同步 JavaScript
→ 仍然必須 run-to-completion,可能卡住主執行緒
例如這段程式在 await 後才開始大量計算:
async function buildReport() {
const response = await fetch('/api/report');
const rows = await response.json();
for (const row of rows) {
// 很重的同步計算
calculateExpensiveSummary(row);
}
}
等待 request 與讀取 body 時,畫面仍能回應;但一旦 response.json() fulfilled,迴圈會在同一個 continuation 裡同步跑到結束。資料量夠大時,使用者點擊與重繪仍必須等待。
因此 await 的價值不是把任何工作自動變快或移到背景,而是讓「尚未有結果」的那段時間不要白白佔住 JavaScript。理解這個邊界後,就能繼續把同一套控制流程模型套用到一筆一筆出現的非同步資料。
async function 被呼叫
→ 立即同步執行到第一個 await
遇到 await
→ 暫停目前 function 的後半段、交還控制權
Promise settled
→ 將 continuation 排進 microtask
microtask 執行
→ 回到 await 後;fulfilled 得到值,rejected 像 throw 一樣進入 catch
| 容易誤解 | 更準確的說法 |
|---|---|
| 呼叫 async function 後,整個 function 都晚點才開始 | 它會立刻跑到第一個 await。 |
await 暫停整個 JavaScript |
它只暫停目前 async function 的後續程式。 |
| 已 fulfilled 的 Promise 不會真的等待 | await 後續仍透過 microtask 執行。 |
await 讓程式不會卡住 |
它只不阻塞等待期間;每段同步工作仍可能卡住。 |
await 讓 consumer 等待一個未來結果。如果資料不是一次完成,或是後端持續發送事件需要在未來才能收到結果,但 consumer 每次都要再問「下一筆好了嗎?」,就可以前面學習到的 Iterator 與 Promise 接起來,變成一段非同步過程。
下一篇就可以運用這兩天的基礎,進入 Async Iterator:next() 理解為什麼會從 { value, done } 變成 Promise<IteratorResult>囉!
MDN — awaitawait 的 control flow、一般值/thenable 處理與 microtask 順序。
ECMAScript 2025 — AwaitAwait 如何保存並在 Promise settled 後恢復 async execution context。
ECMAScript 2025 — Async Functions Abstract Operations
async function 如何建立 Promise capability 與可恢復的 execution context。
MDN — JavaScript execution model
stack、job queue 與 run-to-completion 的整體模型。
Day 10|重新看 Promise:它不是非同步魔法,而是結果的狀態容器
Promise state 與 reaction job 的背景。