Async / Await
async function
หัวข้อที่มีชื่อว่า “async function”คีย์เวิร์ด async หน้าการประกาศฟังก์ชันทำให้กลายเป็น async function ฟังก์ชัน async คืน Promise เสมอ — แม้คุณจะ return ค่าธรรมดา ค่านั้นจะถูกห่อด้วย Promise.resolve(value) โดยอัตโนมัติ
async function greet() { return 'hello';}
greet().then(function(v) { console.log(v); }); // helloภายในฟังก์ชัน async await หยุดการทำงานชั่วคราวจนกว่า Promise จะ settle แล้วทำงานต่อด้วยค่าที่ resolve มา ส่วนที่เหลือของโปรแกรมยังทำงานต่อระหว่างที่ await
async function loadUser() { var user = await fetchUser(42); // pauses here until fetchUser resolves var posts = await fetchPosts(user.id); return posts;}การจัดการ error
หัวข้อที่มีชื่อว่า “การจัดการ error”ห่อ expression await ด้วย try/catch เพื่อจัดการการ reject:
async function run() { try { var data = await fetchData(); console.log(data); } catch (err) { console.error('Failed:', err.message); }}Sequential vs concurrent
หัวข้อที่มีชื่อว่า “Sequential vs concurrent”การเรียก await สองครั้งแบบเรียงลำดับจะทำงานทีละอัน — อันที่สองเริ่มหลังอันแรกทำเสร็จเท่านั้น:
function fetchA() { return Promise.resolve('A'); }function fetchB() { return Promise.resolve('B'); }
var a = await fetchA(); // waitsvar b = await fetchB(); // then waits againหากต้องการรันทั้งสองพร้อมกัน ให้เริ่ม Promise ก่อนแล้ว await พร้อมกัน:
function fetchA() { return Promise.resolve('A'); }function fetchB() { return Promise.resolve('B'); }
var [a, b] = await Promise.all([fetchA(), fetchB()]);forEach(async …) ไม่ได้ทำอย่างที่หน้าตามันดูเลย
หัวข้อที่มีชื่อว่า “forEach(async …) ไม่ได้ทำอย่างที่หน้าตามันดูเลย”Array.prototype.forEach ถูกออกแบบมาสำหรับ callback แบบ synchronous — มันไม่สนใจว่า callback คืนอะไร และไม่รออะไรเลย การส่งฟังก์ชัน async ให้มันไม่ได้เปลี่ยนสิ่งนั้น: forEach ยิง callback ทั้งหมด เพิกเฉยต่อ Promise ที่แต่ละตัวคืนมา แล้วทำงานต่อทันที
บรรทัด “finished” พิมพ์ก่อนบรรทัด forEach saw: ใด ๆ ทั้งนั้น — forEach เอง return แบบ synchronous ทันทีที่ยิง callback ครบทั้งสามตัว นานก่อนที่ await ตัวไหนจะ settle เลย ทีนี้ลองดูว่าเกิดอะไรขึ้นเมื่อ Promise ที่ถูกเพิกเฉยตัวหนึ่ง reject:
กด Run: ในแท็บ browser sandbox นี้ การ reject จะถูกกลืนไปเงียบ ๆ — คุณจะเห็น saved a แล้วไม่มีอะไรสำหรับ bad เลย และแท็บก็ยังทำงานต่อไป (browser จริงจะยิง event unhandledrejection แบบเงียบ ๆ ที่คุณจะเห็นก็ต่อเมื่อเปิด devtools) รันโค้ดชุดเดียวกันนี้เป็นสคริปต์ Node ธรรมดา ผลลัพธ์ต่างออกไปมากและโหดกว่า: unhandled rejection ทำให้โปรเซส crash โดยค่าเริ่มต้น ก่อนที่ item === 'c' จะมีโอกาสได้ทำงานด้วยซ้ำ — item เสียตัวเดียวทำให้ทั้ง batch ล่ม ไม่ใช่แค่ iteration ของมันเอง
เทียบกับ for...of ที่ใช้ await ซึ่งรอทีละขั้น (เรียงลำดับ และ error ส่งต่อผ่าน try/catch ปกติได้) กับ Promise.all(items.map(fn)) ที่รันทุกอย่างพร้อมกันและรอจริง ๆ จนครบ:
ทั้ง for...of และ Promise.all(map) รอจริง ๆ — บรรทัด “really finished” พิมพ์ก็ต่อเมื่อทุก item เสร็จแล้วเท่านั้น และการ reject ข้างในทั้งสองแบบเป็น rejection ปกติที่ catch ได้ ไม่ใช่ถูกเพิกเฉยเงียบ ๆ
ส่ง context ข้าม await: AsyncLocalStorage
หัวข้อที่มีชื่อว่า “ส่ง context ข้าม await: AsyncLocalStorage”request ID ที่ตั้งไว้ตอนต้น handler จะหายไปเมื่อ await ชั้นในกลับมาทำงานต่อ — เว้นแต่คุณเก็บมันไว้ใน AsyncLocalStorage Node 24 สร้างมันใหม่บน AsyncContextFrame ซึ่งส่ง store ต่อผ่านทุก await, timer และ callback โดยมี overhead น้อยกว่า implementation แบบ async-hooks เดิมมาก เดโมด้านล่างรัน “request” พร้อมกันสองตัวที่มี id และ delay ต่างกัน แล้วยืนยันว่าไม่มีตัวไหนเห็น context ของอีกตัวเลย:
Output (verified on Node 24)
[req-1] start
[req-2] start
[req-2] after await, still has the right id
[req-1] after await, still has the right id
all requests finishedแม้ callback ของ req-2 จะกลับมาทำงานก่อน (timer 5 ms ของมันยิงก่อน timer 20 ms ของ req-1) getStore() ของแต่ละ request ก็ยังคืน { id } ของตัวเองเสมอ — context เดินตามสาย await ที่มันเริ่มไว้ ไม่ใช่ตามลำดับที่ callback บังเอิญยิง
เดโมที่รันได้
หัวข้อที่มีชื่อว่า “เดโมที่รันได้”สนิปเป็ตด้านล่างใช้ตัวช่วย fakeFetch (อิง setTimeout) เพื่อจำลองการเรียก async โดยเรียกแบบเรียงลำดับก่อน แล้วเรียกทั้งสองแบบขนานด้วย Promise.all
ผลลัพธ์: Sequential: A then B, แล้ว Parallel: A and B
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”async/await คือ syntax sugar บน Promise ไม่ใช่กลไกใหม่ — await expr แปลว่า “หยุดฟังก์ชันนี้ตรงนี้ กลับมาทำงานต่อด้วยค่าที่ resolve ของ expr เมื่อมัน settle แล้ว และปล่อยให้ทุกอย่างอื่นทำงานระหว่างนั้น” เพราะข้างใต้ยังเป็น Promise อยู่ อะไรก็ตามที่คาดหวัง callback แบบ Promise (.then, Array.prototype.forEach, .map ที่ไม่ตามด้วย Promise.all) ยังได้รับ Promise จาก async function อยู่ดี และจะไม่รอมันให้อัตโนมัติ — await หยุดแค่ฟังก์ชันที่มันอยู่ข้างในเท่านั้น ไม่เคยหยุดผู้เรียกฟังก์ชันที่ลืมส่ง Promise กลับไปให้
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”async/await ทำให้โค้ด async อ่านจากบนลงล่างเหมือนโค้ด synchronous ซึ่งเป็นเหตุผลเดียวกับที่มันซ่อนสองเรื่องที่คุณต้องเติมกลับเข้าไปเอง: concurrency (การ await เรียงลำดับอ่านง่ายแต่ช้า — ต้องเลือกใช้ Promise.all เองถึงจะเร็ว) และรูปแบบการวนซ้ำ (ลูป for...of อ่านเหมือนเดิมไม่ว่า body จะเป็น async หรือไม่ จึงไม่มีสัญญาณเตือนสายตาเลยว่า items.forEach(async fn) ทำลายสมมติฐานนั้นแบบเงียบ ๆ) ความอ่านง่ายที่ได้มาเป็นของจริง แต่มันย้ายภาระ “โค้ดนี้รอจริงไหม” ไปที่การอ่าน iteration method แทนที่จะเป็นคีย์เวิร์ด await
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “ถ้าฟังก์ชันเป็น async การเรียกมันจะรอจนเสร็จเสมอ” รอก็ต่อเมื่อผู้เรียก
awaitการเรียกนั้น (หรือ chain.then) เท่านั้น การเรียก async function โดยไม่awaitแค่เริ่มมันแล้วทำงานต่อทันที — เหมือนกับการเรียกฟังก์ชันอื่นที่ return Promise ทุกประการ - “
forEachก็แค่ for loop เฉย ๆ ดังนั้น await ข้างในทำงานแบบเดียวกัน” ไม่จริง —forEachไม่เคยดูว่า callback ของมัน return อะไร ดังนั้น Promise ของ callback แบบ async (และ rejection ข้างในมัน) จึงตัดขาดจากการเรียกforEachเองโดยสิ้นเชิง - “AsyncLocalStorage เป็น global state ดังนั้น request ที่รันพร้อมกันอาจรั่วเข้าหากันได้” ตรงข้ามเลย โดยการออกแบบ — การเรียก
als.run(store, fn)แต่ละครั้งสร้าง context ที่แยกจากกัน ตามสายawaitของตัวเองเท่านั้น ซึ่งเป็นสิ่งที่เดโมด้านบนพิสูจน์ให้เห็นสำหรับสอง request ที่รันพร้อมกัน
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”bug ที่พบบ่อยที่สุดในโค้ด async ที่ AI สร้างคือการลืม await การเรียกที่ return Promise — โค้ดยังทำงานได้ (ไม่มีอะไร throw ตรงจุดเรียก) แค่ไม่รอผลลัพธ์ ทำให้ bug โผล่ทีหลังเป็น “ข้อมูลยังไม่ถูกบันทึก” หรือ “response กลับมาว่างเปล่า” ซึ่งไกลจากคีย์เวิร์ดที่หายไปมาก
🐛 หา bug ในโค้ดนี้
async function saveUser(user) {validateUser(user); // synchronous, finedb.save(user); // BUG: db.save returns a Promise — this call is not awaitedreturn { ok: true };}เฉลย
db.save(user) return Promise แต่ไม่มีอะไร await หรือ return มันเลย saveUser return { ok: true } ทันที — อาจจะก่อนที่การบันทึกจะไปถึงฐานข้อมูลด้วยซ้ำ และแน่นอนว่าก่อนที่คุณจะรู้ว่ามันล้มเหลวหรือไม่ เพราะการ reject จาก db.save จะกลายเป็น unhandled rejection แทนที่จะเป็น error ที่ถูก catch วิธีแก้คือ await db.save(user); (หรือ return db.save(user).then(() => ({ ok: true })) ถ้าต้องการให้ฟังก์ชันไม่เป็น async) — อะไรก็ตามที่ return Promise และสำคัญต่อความถูกต้อง ต้องถูก await หรือ return เสมอ