ข้ามไปยังเนื้อหา

Promises

Promise คือออบเจ็กต์ที่แทนค่าที่จะพร้อมใช้งานในอนาคต มีสามสถานะ:

  • Pending — งานยังดำเนินอยู่
  • Fulfilled — งานสำเร็จ Promise ถือค่าผลลัพธ์ไว้
  • Rejected — งานล้มเหลว Promise ถือ error ไว้

Promise เปลี่ยนจาก pending ไปเป็น fulfilled หรือ rejected — แล้วไม่ย้อนกลับ เมื่อ settle แล้วจะคงอยู่อย่างนั้น

var p = new Promise(function(resolve, reject) {
// Call resolve(value) to fulfill, or reject(error) to reject
resolve('hello');
});

.then(onFulfilled) คืน Promise ใหม่ ทำให้ chain ขั้นตอนต่อกันได้ .catch(onRejected) จัดการการ reject .finally(fn) ทำงานไม่ว่าผลจะเป็นอย่างไร — มีประโยชน์สำหรับการ cleanup

function fetchData() {
return Promise.resolve({ id: 1 });
}
function transform(data) {
return { ...data, done: true };
}
fetchData()
.then(function(data) { return transform(data); })
.then(function(result) { console.log('Done:', result); })
.catch(function(err) { console.error('Failed:', err.message); })
.finally(function() { console.log('Cleanup'); });
Combinatorพฤติกรรม
Promise.all(arr)resolve เมื่อทุกตัว resolve; reject ทันทีที่มีตัวแรก reject
Promise.race(arr)settle ด้วย Promise ตัวแรกที่ settle (ไม่ว่า fulfilled หรือ rejected)
Promise.allSettled(arr)resolve เมื่อทุกตัว settle; ให้ผลของแต่ละตัว (ไม่เคย reject)
Promise.any(arr)resolve ด้วยตัวแรกที่ fulfilled; reject เฉพาะเมื่อทุกตัว reject

สนิปเป็ตสร้าง Promise สองตัวที่ resolve ทันทีด้วยค่าคงที่ chain ด้วย .then ใช้ Promise.all และเพิ่ม .finally สำหรับ cleanup

JavaScript

ผลลัพธ์จะเป็นเสมอ: Fetched: data-A, แล้ว All done: data-A and data-B, แล้ว Finally: cleanup

ปรับพฤติกรรมนี้ได้ด้วย flag --unhandled-rejections ถ้าจำเป็น แต่ throw (ค่าเริ่มต้น) มักเป็นตัวเลือกที่ถูกต้องสำหรับโค้ดใหม่แทบทุกครั้ง:

โหมดพฤติกรรม
throw (ค่าเริ่มต้นตั้งแต่ Node 15)ยก uncaught exception และทำให้โปรเซส crash
strictเหมือน throw แต่ rejection ที่เกิดระหว่างโปรเซสกำลังปิดตัวก็ทำให้ crash ด้วย
warnlog คำเตือนไปที่ stderr ไม่ crash — ค่าเริ่มต้นเดิมก่อน 15 ตอนนี้ไม่แนะนำแล้ว
noneปิดคำเตือนไปเลย

เมื่อ Promise ตัวหนึ่งที่ส่งให้ Promise.all reject ตัว Promise.all เอง settle (reject) ทันที — แต่มันไม่ยกเลิกหรือหยุดงานที่อยู่หลัง Promise ตัวอื่น การทำงานที่แท้จริงของมัน (timer, fetch, database query) ยังทำงานต่อจนเสร็จ แค่ไม่มีใครฟังผลลัพธ์แล้วตอนที่มันเสร็จ:

Node.js
Output (verified on Node 24)
Promise.all rejected immediately: fast task failed
slow task finished — still ran after Promise.all had already rejected

Promise.all reject ทันทีที่ input ตัวแรก reject แต่ setTimeout ของ slowTask ถูกตั้งไปแล้ว — มันยิงอีก 30 ms ต่อมาไม่ว่าจะยังไง และการเรียก resolve ของมันก็ไม่มีผลอะไร (Promise รวมตัวนั้น settle ไปแล้ว) ถ้าต้องการทั้งผลลัพธ์และทั้งสถานะของทุกตัวแทนที่จะออกก่อนกำหนด ใช้ Promise.allSettled ซึ่งไม่เคย reject และให้ออบเจ็กต์ { status, value } หรือ { status, reason } ต่อหนึ่งอินพุต เมื่อทุกตัวเสร็จแล้ว:

// @skip-verify illustrative — taskA/taskB/taskC are placeholders for your own Promise-returning functions
Promise.allSettled([taskA(), taskB(), taskC()]).then(function (results) {
results.forEach(function (r) {
if (r.status === 'fulfilled') console.log('ok:', r.value);
else console.log('failed:', r.reason.message);
});
});

Promise.race settle — ไม่ว่า fulfilled หรือ rejected — ด้วย input ตัวไหนก็ตามที่ settle ก่อน Promise.any เพิกเฉยต่อการ reject และ resolve ด้วยการ fulfilled ตัวแรก จะ reject ก็ต่อเมื่อทุกตัว reject เท่านั้น:

Node.js
Output (verified on Node 24)
race rejected: fast rejection
any resolved: slow fulfillment

race settle ด้วยสิ่งที่เสร็จก่อน — ในที่นี้คือ fast ที่ reject ไปแล้ว — แม้ว่าจะมี fulfillment กำลังมาก็ตาม any ข้ามการ reject นั้นไปและรอความสำเร็จตัวแรก

ไม่ว่าจะเป็น all, race, allSettled หรือ any ล้วนยกเลิกงานที่แพ้ไม่ได้ด้วยตัวเอง — setTimeout หรือ fetch ที่กำลังทำงานอยู่จะทำงานต่อไปเรื่อย ๆ เว้นแต่มีอะไรบอกให้มันหยุดชัดเจน นั่นคือหน้าที่ของ AbortController: ส่ง signal ของมันเข้าไปในสิ่งที่รองรับการยกเลิก (fetch, fs promises หรือโค้ดของคุณเองผ่าน listener 'abort') แล้วเรียก controller.abort() เมื่อไม่สนใจผลลัพธ์แล้ว:

Node.js
Output (verified on Node 24)
rejected: aborted: client gave up after 20ms

การ abort ที่ 20 ms มาก่อน timer 100 ms และ clearTimeout ก็หยุดงานที่ค้างอยู่ได้จริง (ต่างจากตัวอย่าง Promise.all ข้างบน) Promise จึง reject ด้วยเหตุผลของการ abort แทนที่จะ resolve เลย

new Promise(executor) มีไว้เพื่อห่อ API ที่ยังไม่ใช่ Promise อยู่แล้ว — API แบบ callback, event, timer ถ้าสิ่งที่อยู่ใน executor return Promise อยู่แล้ว การห่อมันอีกชั้นไม่ได้เพิ่มอะไรนอกจากความเสี่ยง (ลืม reject, กลืน rejection):

// Needed: fs.readFile is callback-based, not Promise-based
function readFileAsPromise(path) {
return new Promise(function (resolve, reject) {
require('node:fs').readFile(path, 'utf8', function (err, data) {
if (err) reject(err); else resolve(data);
});
});
}
// Not needed: fetch already returns a Promise — this just adds risk
function getJson(url) {
return new Promise(function (resolve, reject) {
fetch(url).then(resolve, reject); // just: return fetch(url)
});
}

Promise คือใบเสร็จของค่าที่ยังไม่พร้อม โดยมีหนึ่งในสามสถานะเขียนอยู่บนมันตลอดไปเมื่อ settle แล้ว Combinator ไม่ได้เปลี่ยนวิธีที่ Promise แต่ละตัว settle — มันแค่อธิบายเมื่อไหร่จะหยุดรอแล้วดูใบเสร็จ: all หยุดที่ rejection ตัวแรก allSettled รอทุกใบเสร็จไม่ว่าผลจะเป็นอย่างไร race หยุดที่การ settle ตัวแรกไม่ว่าแบบไหน any หยุดที่ความสำเร็จตัวแรก ไม่มีตัวไหนย้อนกลับไปยกเลิกงานที่ Promise นั้นแทนอยู่ได้ มีแต่ AbortController (หรือกลไกการยกเลิกที่ API นั้นรองรับเองจริง ๆ) เท่านั้นที่ทำได้

Promise.all คือค่าเริ่มต้นที่ถูกต้องเมื่อต้องการทุกผลลัพธ์และความล้มเหลวจุดไหนก็ควรหยุดงานทั้งหมด — แต่มันทิ้งข้อมูลความสำเร็จบางส่วน (ตัวไหนทำสำเร็จจริง) และไม่ปล่อย resource ที่ตัวแพ้ใช้อยู่ allSettled เก็บข้อมูลนั้นไว้ได้ แต่ต้องแลกกับการรอ input ที่ช้าที่สุดเสมอ แม้จะรู้อยู่แล้วว่ามันจะพัง AbortController เพิ่มการยกเลิกจริง แต่ใช้ได้เฉพาะโค้ดที่เขียนมาให้เคารพมันเท่านั้น — การใส่ signal ผ่าน fetch ทำง่าย แต่การร้อยมันผ่านฟังก์ชันที่ทำงานนานของคุณเองต้องมีวินัยทุกจุดที่มี await หรือ timer

  • “Promise.all reject แปลว่างานอื่นหยุดแล้ว” มันไม่ได้หยุด — แค่ไม่มีใครฟังผลลัพธ์แล้ว side effect ของมัน (การเขียนไฟล์, การเรียกเก็บเงิน, log line) ยังเกิดขึ้นอยู่ดี
  • “Promise.race เลือกผลลัพธ์ที่สำเร็จเร็วที่สุด” มันเลือกสิ่งที่ settle ก่อน ไม่ว่าจะสำเร็จหรือล้มเหลว — การ reject ที่เร็วชนะการ resolve ที่ช้าทุกครั้ง
  • “การห่อค่าด้วย new Promise(…) เป็นวิธีทำให้อะไรก็ตาม async-friendly เสมอ” ใช้ได้แค่ตอนที่สิ่งที่ห่ออยู่ยังไม่ใช่ Promise การห่อ Promise ด้วย Promise อีกชั้นซ้ำซ้อนและเป็นจุดที่มักทำ error handling หายไปโดยไม่ตั้งใจ

โค้ดที่ AI สร้างมักยิง Promise.all (หรือฟังก์ชันที่ return Promise ตรง ๆ) โดยไม่มี .catch() ที่ไหนใน chain เลย แล้วผู้เรียกก็ไม่ await มันใน try/catch ด้วย — ซึ่งเป็นรูปแบบเดียวกับที่ทำให้เกิด crash ตามที่อธิบายใน Callout ด้านบนเป๊ะ ๆ

🐛 หา bug ในโค้ดนี้

function loadAllUsers(ids) {
return Promise.all(ids.map(function (id) { return fetchUser(id); }));
// no .catch() here...
}
loadAllUsers([1, 2, 3]);
// ...and the caller doesn't await it or attach .catch() either
เฉลย

ถ้า fetchUser(id) ตัวไหน reject Promise.all ก็ reject ด้วย — และไม่มีที่ไหนในไฟล์นี้เลยที่แนบ rejection handler ไว้ บน Node 15+ นี่คือ unhandled rejection ซึ่งทำให้โปรเซส crash โดยค่าเริ่มต้น (--unhandled-rejections=throw) ไม่ใช่แค่ error ที่ถูกเพิกเฉยเงียบ ๆ วิธีแก้คือแนบ .catch() ตรงจุดที่สร้าง Promise หรือให้ผู้เรียก await มันใน try/catch — อย่าปล่อยให้ Promise chain ไม่มี rejection handler อยู่ตรงไหนเลยตลอดทาง

สถานะใดของ Promise หมายถึงงานสำเร็จเรียบร้อย?
Promise.all ทำอะไรเมื่อ Promise ตัวหนึ่งในอินพุต reject?
combinator ใด resolve เมื่อ Promise อินพุตทุกตัว settle และไม่เคย reject?
โค้ดนี้พิมพ์อะไร?
var fast = Promise.reject(new Error('fast rejection'));
var slow = new Promise(function (resolve) {
  setTimeout(function () { resolve('slow fulfillment'); }, 20);
});
Promise.any([fast, slow]).then(function (v) { console.log(v); });
ฟังก์ชันหนึ่ง return Promise.all(ids.map(fetchUser)); โดยไม่มี .catch() และผู้เรียกก็ไม่ await ผลลัพธ์ด้วย ถ้า fetchUser ตัวหนึ่ง reject จะเกิดอะไรขึ้น?
ทำไมการห่อ fetch(url) ด้วย new Promise((resolve, reject) => fetch(url).then(resolve, reject)) ถึงนับเป็น anti-pattern?