一句摘要:回到 Promise 的核心:executor 同步執行、狀態只能前進,以及 reaction job 如何安排後續通知。
前置知識:理解函式、callback 與 .then() 的基本寫法即可;這篇會從 new Promise() 重新建立運作模型。
學習路線:從 executor 的同步執行、Promise 狀態轉換,一路追到 reaction job 的排程過程。
標籤:Promise Microtask Event Loop 語言規格
在同步程式裡,呼叫函式時通常能在當下取得回傳值。
但網路回應、timer 或 event 的結果不一定已經出現。JavaScript 需要一種穩定的表示方式,記錄「這個結果未來會成功或失敗」。
Promise 就是這個未來結果的表示方式。
也因為初學 JavaScript 時需要用到非同步流程時,都會和 Promise 搭上邊,看著看著很容易被誤解成「幫 JavaScript 執行非同步工作的魔法」,但如果用久了仔細去看會慢慢體會到:
Promise 不負責做網路請求、等待計時器或監聽點擊事件;它負責保存一個未來結果的狀態,並在結果出現後通知等待它的人。
今天就從 new Promise() 重新看一次這個流程,並用 TC39 規範對照幾個關鍵步驟,來探索以前被忽略的語言細節,也會加深自己對 Promise 的感覺。

看到 Promise 時,不要直接把它理解成「裡面的程式全部稍後執行」,可ㄧ可以分成三層看:
1. executor 是否立刻執行?
→ 是,同步。
2. 結果何時出現?
→ 看裡面是否呼叫 timer、fetch、event、Worker 等未來事件來源。
3. .then() 後續何時執行?
→ Promise settled 後,後續 reaction 會被排程,稍後才執行。
即使 Promise 已經立刻完成,.then() 仍會晚一點執行:
console.log("A:同步開始");
const promise = new Promise((resolve) => {
console.log("B:executor 同步執行");
setTimeout(() => {
console.log("D:timer callback");
resolve();
console.log("E:resolve 後繼續執行");
}, 0);
});
promise.then(() => {
console.log("F:then handler");
});
console.log("C:同步結束");
// A:同步開始
// B:executor 同步執行
// C:同步結束
// D:timer callback
// E:resolve 後繼續執行
// F:then handler
因此 Promise 的工作不是「讓所有程式非同步」,而是用統一介面保存未來結果,並安排後續的通知。
接下來會看到幾個名稱很長的抽象操作,也就是 TC39 裡面到底規範哪些流程,不過耐著性子看完後發現跟家電說明書一樣😆,以後怎麼用會更清楚,忘了再翻閱也行。
這些名稱不是開發者可以直接呼叫的 JavaScript API,而是 ECMAScript 用來描述引擎必須如何處理 Promise 的步驟。
先把它們當成本篇的閱讀地圖即可;後面會再對照 Promise 的狀態與範例,把排程流程展開。
注意到一開始建立 Promise 時,就已經對這個容器標記 pending 狀態
| 順序 | TC39 規格步驟 | 白話理解 |
|---|---|---|
| 1 | Promise constructor 建立內部欄位,狀態設為 pending |
先建立等待結果的容器。 |
| 2 | CreateResolvingFunctions |
建立這個 Promise 專屬的 resolve 與 reject。 |
| 3 | Call(executor, ..., [resolve, reject]) |
立刻同步執行 executor。 |
.then() reaction 的排程路徑:這邊就是常見我們在接續非同步操作完成狀態結果
| 順序 | TC39 抽象操作 | 白話理解 |
|---|---|---|
| 1 | PerformPromiseThen |
登記 .then() 後要執行的 reaction。 |
| 2(視情況) | TriggerPromiseReactions |
如果登記時還是 pending,就在結果出現後取出 reactions。 |
| 3 | NewPromiseReactionJob |
把 handler 包成稍後執行的 Job。 |
| 4 | HostEnqueuePromiseJob |
把 Job 交給執行環境排程。 |
如果 Promise 已經 settled,第 2 步會被略過,路徑就是 1 → 3 → 4。
它通常出現在
resolve()傳入另一個 Promise 或 thenable 時:外層 Promise 不會立刻把它當成成功值,而是接管並跟隨它最後的結果。
| TC39 抽象操作 | 何時出現 | 白話理解 |
|---|---|---|
NewPromiseResolveThenableJob |
resolve() 收到 thenable |
把呼叫 thenable then 的工作排成 Job,再交給 HostEnqueuePromiseJob。 |
先看這段:
console.log("A");
new Promise((resolve) => {
console.log("B");
resolve();
});
console.log("C");
輸出是:
A
B
C
new Promise() 接收的函式稱為 executor。它不是稍後才執行,而是在建立 Promise 的當下就立刻同步呼叫。
new Promise(executor)
↓
立刻執行 executor(resolve, reject)
↓
new Promise(...) 才回傳 Promise 物件
TC39 的 Promise constructor 流程也清楚寫出:先建立一個狀態為
pending的 Promise,再透過Call(executor, ...)呼叫 executor,並把resolve、reject傳進去。TC39:Promise constructor
以 timer 為例:
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve("時間到了");
}, 1000);
});
實際上分成兩段:
JavaScript 現在同步執行
↓
executor 呼叫 setTimeout,註冊 callback
↓
瀏覽器或 Node.js 的執行環境負責計時 // 交棒給 host 提供的 api 執行
↓
一秒後,執行環境呼叫 timer callback
↓
callback 呼叫 resolve("時間到了")
↓
Promise 改變狀態
Promise 沒有「等一秒」;它只是一開始處於 pending,直到 callback 呼叫 resolve() 才記錄結果。
同樣的概念也適用於:
| API | executor 裡做的事 | 未來由誰通知結果 |
|---|---|---|
setTimeout |
註冊 timer callback | runtime 的計時器機制 |
| DOM event | addEventListener() |
瀏覽器事件系統 |
fetch |
發起 request | 瀏覽器/Node.js 網路實作 |
| Web Worker | postMessage() |
Worker 的 message event |
fetch、DOM 與 timer 都不是 ECMAScript 語言規範本身定義的功能;它們由瀏覽器或 Node.js 等 host environment 提供。Promise 則提供一種一致的「未來結果」介面。
一個 Promise 只有三種狀態:
pending
├─ resolve(value) → fulfilled
└─ reject(error) → rejected
一旦離開
pending,就不能再改變:
const promise = new Promise((resolve, reject) => {
resolve("第一個結果");
reject(new Error("太晚了"));
resolve("也太晚了");
});
promise.then((value) => {
console.log(value); // 第一個結果
});
TC39 用內部欄位描述這個模型:
| 內部欄位 | 白話意思 |
|---|---|
[[PromiseState]] |
pending、fulfilled 或 rejected |
[[PromiseResult]] |
成功值或失敗原因 |
[[PromiseFulfillReactions]] |
成功後要執行的 .then() handler 清單 |
[[PromiseRejectReactions]] |
失敗後要執行的 .catch() handler 清單 |
這些不是你能直接讀取的 property,而是規範用來定義 Promise 行為的內部模型。TC39:Promise internal slots
resolve() 不代表 .then() 立刻執行再看一個經典順序題:
console.log("A");
new Promise((resolve) => {
console.log("B");
resolve("完成");
}).then((value) => {
console.log("C", value);
});
console.log("D");
輸出是:
A
B
D
C 完成
拆開來看:
1. 印出 A
2. executor 同步執行,印出 B
3. resolve("完成"),Promise 變 fulfilled
4. .then() 的後續工作被排程
5. 目前同步程式繼續,印出 D
6. 同步程式清空後,才執行 .then(),印出 C 完成
這個例子中,
resolve("完成")先讓 Promise 變成 fulfilled 並保存結果;後來呼叫.then()時,才建立並排程 Reaction Job。因此 handler 雖然是同步行為不會插進目前的 call stack,而會等同步程式結束後執行。
至於 Promise 還是 pending 時,reaction 會先存在哪裡,以及 settled 與 pending 的排程路徑有何不同,會在第七節展開。
const promise = new Promise(() => {
throw new Error("出錯了");
});
promise.catch((error) => {
console.log(error.message); // 出錯了
});
你沒有手動寫 reject(error),Promise constructor 仍會把它轉成 rejected Promise。
規範的流程是:呼叫 executor 後,檢查執行結果是否為 abrupt completion;如果是,就呼叫內部建立的 reject()。TC39:Promise constructor
概念上像這樣:
function createPromise(executor) {
return new Promise((resolve, reject) => {
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
});
}
這表示 Promise constructor 建立的不只是 resolve 與 reject 兩個入口;它也建立了一道邊界,將 executor 當下拋出的例外轉成這個 Promise 的失敗結果。
resolve(另一個 Promise) 又發生了什麼?resolve()` 不只接受一般值,也能接受另一個 Promise:
const inner = Promise.resolve("商品資料");
const outer = new Promise((resolve) => {
resolve(inner);
});
outer.then((value) => {
console.log(value); // 商品資料
});
outer 不會立刻 fulfilled 成「一個 Promise 物件」;它會跟隨 inner 的最終狀態:
inner fulfilled → outer fulfilled
inner rejected → outer rejected
這個行為常被稱為 promise resolution/thenable assimilation。規範遇到帶有可呼叫 then 的物件時,會建立 NewPromiseResolveThenableJob,非同步處理這個跟隨流程。TC39:NewPromiseResolveThenableJob
從執行順序可以看出,assimilation 本身也會形成一次排程切分:
const thenable = {
then(resolve) {
console.log("thenable.then");
resolve("完成");
},
};
console.log("A");
Promise.resolve(thenable).then((value) => {
console.log(value);
});
console.log("B");
// A
// B
// thenable.then
// 完成
Promise.resolve(thenable) 會稍後呼叫 thenable 的 then,但這不代表它正在等待網路或 timer。對照前面的外部事件,可以這樣區分:
| 情況 | 稍後執行的原因 | Promise 的角色 |
|---|---|---|
| Thenable assimilation | 規格要求將陌生 thenable 的 then 包成 Job |
接管另一個 Promise-like 結果 |
| Timer、網路或 event | host environment 正在等待未來事件 | 事件完成後保存結果,再排程 reaction |
現在已經知道 Promise 的狀態與 .then() 的作用,可以把前面的 TC39 地圖展開。呼叫 .then() 時,Promise 可能已經 settled,也可能還是 pending;兩種情況建立 reaction job 的時機不同。
呼叫
.then(),可以先把它想成向 Promise 登記:「結果出現時,請執行這個 handler。」,執行這個handler 任務也是需要排隊,不能亂插隊~
如果結果還沒出現,Promise 會先保存這筆登記;如果結果已經存在,就能直接把 reaction 包成 Job。兩條路最後都會把 Job 交給 host environment 排程:

對回規格名稱:
登記:PerformPromiseThen
取出:TriggerPromiseReactions
包成工作:NewPromiseReactionJob
交給執行環境:HostEnqueuePromiseJob
對回規格名稱:PerformPromiseThen 負責登記;pending Promise settled 後,由 TriggerPromiseReactions 取出 reactions;兩條路最後都透過 NewPromiseReactionJob 建立 Job。
Job 建立後,HostEnqueuePromiseJob 才是 ECMAScript 交給 host 的橋接接口;event loop 則負責後續的調度:
因此,「.then() 將 handler 放進 microtask queue」只是簡寫。Promise 還是 pending 時,.then() 只會先保存 reaction;等結果出現後,才建立並排入 Promise Job。在瀏覽器中,HTML 規格會將 Promise Jobs 排入 microtask queue。

把前面的三層模型放進實務情境,最常見的流程是:executor 當下註冊 callback,host environment 在外部等待 API 結果,callback 稍後再呼叫 resolve() 或 reject()。
const promise = new Promise((resolve, reject) => {
apiRequest((error, data) => {
if (error) {
reject(error);
return;
}
resolve(data); // API 回應後才呼叫
});
});
這裡的 apiRequest 代表一個以 callback 回報結果的 API。時間順序是:
new Promise(...)
↓
executor 立刻同步執行
↓
註冊 API callback
↓
executor 執行完畢,Promise 維持 pending
↓
host environment 等待網路結果
↓
API callback 被呼叫
↓
resolve(data) 或 reject(error)
↓
Promise fulfilled 或 rejected
↓
排程 .then() 或 .catch() reaction
不過,這是「等待 API」的使用情境,不是 resolve() 本身的限制。resolve() 也可以在 executor 裡立刻同步呼叫:
const promise = new Promise((resolve) => {
resolve(42); // 同步呼叫
});
console.log("A");
promise.then(console.log);
console.log("B");
// A
// B
// 42
resolve(42) 是同步呼叫,Promise 可以在當下變成 fulfilled;但 .then() handler 仍會透過 Promise Job 稍後執行。
fetch() 又稍微不同:
const promise = fetch("/api/products");
fetch() 會立刻回傳 pending Promise。網路回應抵達後,由瀏覽器或 Node.js 的 fetch 實作完成這個 Promise;應用程式通常看不到它內部何時呼叫對應的 resolve。
等 API 的不是
resolve(),也不是 Promise;是 host environment。API 結果抵達後,host 才透過resolve()或等效的內部機制,把結果交給 Promise
重新記住 Promise 的流程:
1. new Promise() 立刻同步執行 executor
2. executor 註冊或啟動未來工作
3. 未來事件呼叫 resolve / reject
4. Promise 從 pending 走向 fulfilled / rejected
5. .then / .catch 的 reaction job 稍後執行
6. thenable assimilation 只是將陌生 `then` 排成 Job,不代表正在等待網路或 timer
因此:
Promise 不是非同步工作的執行者,Promise 是未來結果的狀態容器與通知機制
Promise 負責表示「未來會有的結果」,但我們還沒回答:等待這個結果時,JavaScript 究竟暫停了什麼?
下一篇就從 await 把 async function 切成前後兩段的控制流程開始。