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

Async / Await

คีย์เวิร์ด 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;
}

ห่อ expression await ด้วย try/catch เพื่อจัดการการ reject:

async function run() {
try {
var data = await fetchData();
console.log(data);
} catch (err) {
console.error('Failed:', err.message);
}
}

การเรียก await สองครั้งแบบเรียงลำดับจะทำงานทีละอัน — อันที่สองเริ่มหลังอันแรกทำเสร็จเท่านั้น:

function fetchA() { return Promise.resolve('A'); }
function fetchB() { return Promise.resolve('B'); }
var a = await fetchA(); // waits
var 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()]);

Array.prototype.forEach ถูกออกแบบมาสำหรับ callback แบบ synchronous — มันไม่สนใจว่า callback คืนอะไร และไม่รออะไรเลย การส่งฟังก์ชัน async ให้มันไม่ได้เปลี่ยนสิ่งนั้น: forEach ยิง callback ทั้งหมด เพิกเฉยต่อ Promise ที่แต่ละตัวคืนมา แล้วทำงานต่อทันที

JavaScript

บรรทัด “finished” พิมพ์ก่อนบรรทัด forEach saw: ใด ๆ ทั้งนั้น — forEach เอง return แบบ synchronous ทันทีที่ยิง callback ครบทั้งสามตัว นานก่อนที่ await ตัวไหนจะ settle เลย ทีนี้ลองดูว่าเกิดอะไรขึ้นเมื่อ Promise ที่ถูกเพิกเฉยตัวหนึ่ง reject:

JavaScript

กด 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)) ที่รันทุกอย่างพร้อมกันและรอจริง ๆ จนครบ:

JavaScript

ทั้ง for...of และ Promise.all(map) รอจริง ๆ — บรรทัด “really finished” พิมพ์ก็ต่อเมื่อทุก item เสร็จแล้วเท่านั้น และการ reject ข้างในทั้งสองแบบเป็น rejection ปกติที่ catch ได้ ไม่ใช่ถูกเพิกเฉยเงียบ ๆ

request ID ที่ตั้งไว้ตอนต้น handler จะหายไปเมื่อ await ชั้นในกลับมาทำงานต่อ — เว้นแต่คุณเก็บมันไว้ใน AsyncLocalStorage Node 24 สร้างมันใหม่บน AsyncContextFrame ซึ่งส่ง store ต่อผ่านทุก await, timer และ callback โดยมี overhead น้อยกว่า implementation แบบ async-hooks เดิมมาก เดโมด้านล่างรัน “request” พร้อมกันสองตัวที่มี id และ delay ต่างกัน แล้วยืนยันว่าไม่มีตัวไหนเห็น context ของอีกตัวเลย:

Node.js
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

JavaScript

ผลลัพธ์: Sequential: A then B, แล้ว Parallel: A and B

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 กลับไปให้

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 ที่รันพร้อมกัน

bug ที่พบบ่อยที่สุดในโค้ด async ที่ AI สร้างคือการลืม await การเรียกที่ return Promise — โค้ดยังทำงานได้ (ไม่มีอะไร throw ตรงจุดเรียก) แค่ไม่รอผลลัพธ์ ทำให้ bug โผล่ทีหลังเป็น “ข้อมูลยังไม่ถูกบันทึก” หรือ “response กลับมาว่างเปล่า” ซึ่งไกลจากคีย์เวิร์ดที่หายไปมาก

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

async function saveUser(user) {
validateUser(user); // synchronous, fine
db.save(user); // BUG: db.save returns a Promise — this call is not awaited
return { 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 เสมอ

async function คืนค่าอะไรเสมอ?
วิธีที่ถูกต้องในการจัดการการ reject จาก Promise ที่ await คือข้อใด?
รูปแบบใดรันสองงาน async พร้อมกันแทนที่จะเรียงลำดับ?
โค้ดนี้พิมพ์อะไร?
function fakeSave(item) {
  return new Promise(function (resolve) {
    setTimeout(function () { resolve(item); }, 0);
  });
}
console.log('start');
['a', 'b'].forEach(async function (item) {
  var r = await fakeSave(item);
  console.log('saved', r);
});
console.log('end');
ฟังก์ชันหนึ่งเรียก db.save(user); โดยไม่ await แล้ว return { ok: true }; ทันที ถ้า db.save เกิด reject ในที่สุด จะเกิดอะไรขึ้น?