Promises
Promise คืออะไร?
หัวข้อที่มีชื่อว่า “Promise คืออะไร?”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');});การ chain ด้วย .then / .catch / .finally
หัวข้อที่มีชื่อว่า “การ chain ด้วย .then / .catch / .finally”.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'); });Promise combinator
หัวข้อที่มีชื่อว่า “Promise combinator”| 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
ผลลัพธ์จะเป็นเสมอ: 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 ด้วย |
warn | log คำเตือนไปที่ stderr ไม่ crash — ค่าเริ่มต้นเดิมก่อน 15 ตอนนี้ไม่แนะนำแล้ว |
none | ปิดคำเตือนไปเลย |
Promise.all ไม่ยกเลิกงานที่เหลือ
หัวข้อที่มีชื่อว่า “Promise.all ไม่ยกเลิกงานที่เหลือ”เมื่อ Promise ตัวหนึ่งที่ส่งให้ Promise.all reject ตัว Promise.all เอง settle (reject) ทันที — แต่มันไม่ยกเลิกหรือหยุดงานที่อยู่หลัง Promise ตัวอื่น การทำงานที่แท้จริงของมัน (timer, fetch, database query) ยังทำงานต่อจนเสร็จ แค่ไม่มีใครฟังผลลัพธ์แล้วตอนที่มันเสร็จ:
Output (verified on Node 24)
Promise.all rejected immediately: fast task failed
slow task finished — still ran after Promise.all had already rejectedPromise.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 functionsPromise.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); });});race กับ any และการยกเลิกด้วย AbortController
หัวข้อที่มีชื่อว่า “race กับ any และการยกเลิกด้วย AbortController”Promise.race settle — ไม่ว่า fulfilled หรือ rejected — ด้วย input ตัวไหนก็ตามที่ settle ก่อน Promise.any เพิกเฉยต่อการ reject และ resolve ด้วยการ fulfilled ตัวแรก จะ reject ก็ต่อเมื่อทุกตัว reject เท่านั้น:
Output (verified on Node 24)
race rejected: fast rejection
any resolved: slow fulfillmentrace settle ด้วยสิ่งที่เสร็จก่อน — ในที่นี้คือ fast ที่ reject ไปแล้ว — แม้ว่าจะมี fulfillment กำลังมาก็ตาม any ข้ามการ reject นั้นไปและรอความสำเร็จตัวแรก
ไม่ว่าจะเป็น all, race, allSettled หรือ any ล้วนยกเลิกงานที่แพ้ไม่ได้ด้วยตัวเอง — setTimeout หรือ fetch ที่กำลังทำงานอยู่จะทำงานต่อไปเรื่อย ๆ เว้นแต่มีอะไรบอกให้มันหยุดชัดเจน นั่นคือหน้าที่ของ AbortController: ส่ง signal ของมันเข้าไปในสิ่งที่รองรับการยกเลิก (fetch, fs promises หรือโค้ดของคุณเองผ่าน listener 'abort') แล้วเรียก controller.abort() เมื่อไม่สนใจผลลัพธ์แล้ว:
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 เลย
Anti-pattern ของ new Promise
หัวข้อที่มีชื่อว่า “Anti-pattern ของ new Promise”new Promise(executor) มีไว้เพื่อห่อ API ที่ยังไม่ใช่ Promise อยู่แล้ว — API แบบ callback, event, timer ถ้าสิ่งที่อยู่ใน executor return Promise อยู่แล้ว การห่อมันอีกชั้นไม่ได้เพิ่มอะไรนอกจากความเสี่ยง (ลืม reject, กลืน rejection):
// Needed: fs.readFile is callback-based, not Promise-basedfunction 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 riskfunction getJson(url) { return new Promise(function (resolve, reject) { fetch(url).then(resolve, reject); // just: return fetch(url) });}Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Promise คือใบเสร็จของค่าที่ยังไม่พร้อม โดยมีหนึ่งในสามสถานะเขียนอยู่บนมันตลอดไปเมื่อ settle แล้ว Combinator ไม่ได้เปลี่ยนวิธีที่ Promise แต่ละตัว settle — มันแค่อธิบายเมื่อไหร่จะหยุดรอแล้วดูใบเสร็จ: all หยุดที่ rejection ตัวแรก allSettled รอทุกใบเสร็จไม่ว่าผลจะเป็นอย่างไร race หยุดที่การ settle ตัวแรกไม่ว่าแบบไหน any หยุดที่ความสำเร็จตัวแรก ไม่มีตัวไหนย้อนกลับไปยกเลิกงานที่ Promise นั้นแทนอยู่ได้ มีแต่ AbortController (หรือกลไกการยกเลิกที่ API นั้นรองรับเองจริง ๆ) เท่านั้นที่ทำได้
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”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 เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ดที่ 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 อยู่ตรงไหนเลยตลอดทาง