Skip to content

บทที่ 09 — Async: Event Loop, Promise และ async/await

← บทที่ 08 | สารบัญ | บทที่ 10: Modules →

🟡 หมายเหตุระดับ: บทนี้คือ พื้นฐาน async ที่จำเป็น สำหรับงาน frontend/backend ทั่วไป — ครอบ event loop, Promise, async/await ในระดับ "เริ่มต้นแบบเจาะลึก" ไม่ใช่ระดับเครื่องยนต์ของ V8 หรือคิว scheduler ของ Node แบบลึก นอกจากนี้ยังครอบ AbortController (ตัวยกเลิกงาน async — ใช้หยุดงาน async ที่ยังไม่เสร็จ เช่น ผู้ใช้ปิดหน้าก่อนโหลดข้อมูลเสร็จ)

หัวข้อระดับสูงกว่านี้ — เช่น async iterator, generator, Worker, SharedArrayBuffer, microtask starvation, structured concurrency — ไม่ต้องสนใจสำหรับมือใหม่ (จะมีลิสต์รวมไว้ท้ายบทถ้าอยากศึกษาเสริมในอนาคต)

นี่คือ บทที่สำคัญที่สุดบทหนึ่ง ของ JavaScript — ถ้าไม่เข้าใจบทนี้ คุณทำเว็บแอปจริงไม่ได้

ทำไมสำคัญ? — เพราะแทบทุกอย่างในเว็บเป็น "async": โหลดข้อมูลจาก server, รออ่านไฟล์, รอ timer — ทั้งหมดต้องใช้ async

อ่านจบบทนี้คุณจะ:

  • เข้าใจ Event Loop อย่างลึกซึ้ง — ว่า async ทำงานยังไง
  • เข้าใจวิวัฒนาการ: callback → Promise → async/await
  • เขียน async/await ได้คล่อง พร้อมจัดการ error
  • ทำงาน async หลายอย่างพร้อมกัน (parallel) ได้

ใช้เวลา 4-5 ชั่วโมง — บทนี้ยาก อ่านช้า ๆ มาก ลองทุกตัวอย่าง


Part 1: ทำไมต้องมี Async

1.1 ปัญหา — JavaScript ทำงานทีละอย่าง

ทบทวนจากบทที่ 00 — JavaScript เป็น single-threaded (มีเธรดเดียว) = มี "มือ" เดียวในการทำงาน ทำได้ทีละอย่างเท่านั้น ทำอย่างหนึ่งเสร็จจึงทำอย่างต่อไปได้

ทีนี้ลองคิด — ถ้าโปรแกรมต้อง "โหลดข้อมูลจาก server" ซึ่งใช้เวลา 2 วินาที:

javascript
// (สมมติ — รันไม่ได้จริง เป็นแค่ตัวอย่างแนวคิด)
console.log("เริ่ม");
const data = โหลดข้อมูลจาก_server();   // ถ้าอันนี้ "รอ" 2 วินาที...
console.log("เสร็จ");

ถ้า JavaScript "ยืนรอ" 2 วินาที — ระหว่างนั้นทั้งโปรแกรมค้าง — ผู้ใช้คลิกอะไรก็ไม่ได้ เว็บแข็งทื่อ

นี่คือสิ่งที่เรียกว่า blocking (การหยุดรอ/ทำให้โปรแกรมค้าง) — และมันยอมรับไม่ได้ในเว็บ

1.2 ทางแก้ — Asynchronous

JavaScript แก้ด้วยแนวคิด asynchronous (async) — แทนที่จะ "ยืนรอ":

"เริ่มงานที่ต้องรอไว้ — แล้วไปทำงานอื่นต่อ — พองานนั้นเสร็จค่อยกลับมาจัดการ"

ทบทวนตัวอย่างพ่อครัวจากบทที่ 00:

  • ลูกค้าสั่ง "ต้มน้ำ 5 นาที"
  • พ่อครัวไม่ยืนจ้องหม้อ — เปิดเตา ตั้งเวลา แล้วไปทำเมนูอื่น
  • พอเวลาครบ มีเสียงเตือน — พ่อครัวค่อยกลับมาจัดการ

→ async = "ตั้งงานไว้ ทำอื่นต่อ พอเสร็จค่อยกลับมา"


Part 2: Event Loop — กลไกเบื้องหลัง async

ก่อนเขียนโค้ด async — ต้องเข้าใจ "Event Loop" ว่ามันทำงานยังไง

2.1 องค์ประกอบ

ก่อนเข้าใจ async ต้องรู้กลไก "event loop" ก่อน — JavaScript ทำงานทีละอย่าง (single thread) แต่จัดการงานที่ต้องรอ (timer, โหลดข้อมูล) ได้โดยส่งให้ Web API ทำเบื้องหลัง แล้วเอา callback กลับเข้าคิวเมื่อเสร็จ แผนภาพด้านล่างแสดงองค์ประกอบทั้งหมด:

2.2 ทดลองให้เห็นภาพ

javascript
console.log("1");
setTimeout(() => console.log("2"), 0);     // ตั้งเวลา 0 มิลลิวินาที!
console.log("3");

คุณคาดว่าได้ 1, 2, 3 ใช่ไหม? — ผิด ผลลัพธ์จริงคือ:

text
1
3
2

ทำไม? — แม้ setTimeout ตั้งเวลา 0 แต่:

  1. console.log("1") — รันทันที → "1"
  2. setTimeout(...) — JavaScript ไม่รันทันที! มันส่ง callback ไปที่ Web APIs (แม้เวลา 0)
  3. console.log("3") — รันทันที → "3"
  4. Call Stack ว่างแล้ว → Event Loop ดึง callback ของ setTimeout มาทำ → "2"

บทเรียนสำคัญ: setTimeout(fn, 0) ไม่ได้แปลว่า "ทำทันที" — มันแปลว่า "ทำหลังจากงาน sync ทั้งหมดเสร็จ"

สิ่งที่เรียกว่า "sync / synchronous" = "โค้ดปกติที่ทำทีละบรรทัดตามลำดับบนลงล่าง" — ตรงข้ามกับ async ที่ "ตั้งงานไว้แล้วไปทำอื่นต่อ"

2.3 Microtask vs Macrotask (งานคิวเล็ก vs งานคิวใหญ่)

ตรงนี้ลึกขึ้นมาหน่อย — รู้จัก 2 คำนี้ก่อน:

  • Macrotask (งานคิวใหญ่) — งานจาก setTimeout, setInterval, event ต่าง ๆ → ทำ "ทีหลัง"
  • Microtask (งานคิวเล็ก) — งานจาก Promise (.then) → ทำ "ก่อน"

คิวของ callback จริง ๆ มี 2 ประเภทนี้ และมีกฎลำดับการทำ:

กฎ: Event Loop ทำ microtask ทั้งหมดให้หมดก่อน แล้วค่อยทำ macrotask

javascript
console.log("1");
setTimeout(() => console.log("2 - macrotask"), 0);
Promise.resolve().then(() => console.log("3 - microtask"));
console.log("4");

// ผลลัพธ์:
// 1
// 4
// 3 - microtask     ← microtask ทำก่อน
// 2 - macrotask     ← macrotask ทำทีหลัง

ตอนนี้แค่รู้ว่า "Promise มาก่อน setTimeout" ก็พอ — ไม่ต้องจำลึก

🚧 ขั้นสูง — มือใหม่ข้ามได้ (อ่านเฉพาะคนที่ debug ปัญหา async จริงเท่านั้น):

  1. คิว microtask จะถูกล้างจนหมด ทุกครั้งระหว่างการทำ macrotask แต่ละตัว — ไม่ใช่แค่อันแรก ถ้าเขียน .then() ซ้อนซ้ำ ๆ ที่คืน Promise ที่เสร็จทันทีตลอดเวลา → จะเกิดอาการที่เรียกว่า "microtask starvation" ("microtask แย่ง CPU จนงานอื่นไม่ได้รัน") — งานคิวใหญ่ เช่น setTimeout หรือการวาดหน้าจอจะไม่ได้รันเลย หน้าจอค้าง
  2. Node.js มี process.nextTick ที่ลำดับความสำคัญสูงกว่า Promise microtask — คิว nextTick จะถูกล้างก่อนเสมอ ใช้ระวัง (ทำให้คิวอื่นอด CPU ได้ง่ายกว่า) — ⚠️ นี่เป็นของเฉพาะ Node เท่านั้น ถ้าลองรันใน browser console จะไม่มี process ให้ใช้เลย (process is not defined) เพราะ browser ไม่มีตัวแปรนี้
  3. queueMicrotask(fn)API มาตรฐานสำหรับสั่ง microtask โดยตรง ใช้แทน Promise.resolve().then(fn) ได้และความหมายตรงกว่า

(last reviewed: 2026-06 — ดู HTML spec: event loop และ Node.js docs เรื่อง event loop ก่อนใช้งานจริง)

2.4 อย่า block Event Loop

เพราะ JavaScript ทำทีละงาน — ถ้าเขียนโค้ดที่ "หนักและนาน" มันจะ block:

javascript
// ❌ อย่าทำ — วน loop หนัก ๆ → เว็บค้างหลายวินาที
let sum = 0;
for (let i = 0; i < 1_000_000_000; i++) {
  sum += i;          // ต้องมี "งานจริง" ใน loop ไม่งั้น engine อาจตัดทิ้ง — เรียกว่า "dead-code elimination" (เอนจิ้นฉลาดพอจะสังเกตว่าโค้ดที่ไม่มีผลลัพธ์จริงเป็นของทิ้งเปล่า จึงลบทิ้งเพื่อความเร็ว)
}
console.log(sum);

ระหว่าง loop นี้ทำงาน — เว็บค้าง คลิกอะไรไม่ได้ → ใช้ async หรือแบ่งงานเป็นชิ้นเล็ก


Part 3: Callback — วิธี async แบบดั้งเดิม

วิธีแรกสุดที่ JavaScript ทำ async คือ callback — "ส่ง function เข้าไป ให้เรียกเมื่อเสร็จ"

3.1 ตัวอย่าง

javascript
console.log("เริ่มโหลด");

setTimeout(() => {
  console.log("โหลดเสร็จ!");      // callback — ทำเมื่อครบ 2 วินาที
}, 2000);

console.log("ทำงานอื่นต่อ");

// ผลลัพธ์:
// เริ่มโหลด
// ทำงานอื่นต่อ
// (รอ 2 วินาที)
// โหลดเสร็จ!

function ที่ส่งเข้า setTimeout = callback — "function ที่ฝากไว้ ให้เรียกทีหลัง"

3.2 ปัญหา — Callback Hell

callback ใช้ได้ดีกับงานเดียว — แต่พอต้องทำหลายงาน "ต่อ ๆ กัน" จะกลายเป็นนรก:

javascript
// สมมติต้อง: โหลด user → โหลด order ของ user → โหลด รายละเอียด order
loadUser(1, (user) => {
  loadOrders(user.id, (orders) => {
    loadOrderDetails(orders[0].id, (details) => {
      loadShipping(details.shippingId, (shipping) => {
        console.log(shipping);
        // ยิ่งซ้อนยิ่งลึก — "pyramid of doom" (โครงสร้างโค้ดที่ซ้อนลึกเหมือนพีระมิด — ยิ่งซ้อนมาก ยิ่งอ่านยาก)
      });
    });
  });
});

โค้ดเยื้องลึกขึ้นเรื่อย ๆ เป็น "พีระมิด" — อ่านยาก จัดการ error ยาก แก้ยาก

นี่เรียกว่า "Callback Hell" — และเป็นเหตุผลที่ Promise ถูกสร้างขึ้น


Part 4: Promise — วิธี async ที่ดีกว่า

Promise = object ที่แทน "ผลลัพธ์ของงานที่จะเสร็จในอนาคต"

💡 เปรียบเทียบ: Promise เหมือน "บัตรคิวร้านอาหาร" — คุณสั่งอาหาร ได้บัตรคิวมา (ยังไม่ได้อาหาร) — บัตรนั้น "สัญญา" ว่าอาหารจะมา — รออาหารเสร็จ (fulfilled) หรือ ร้านบอกทำไม่ได้ (rejected)

4.1 สถานะของ Promise

Promise มี 3 สถานะ:

  • Pending — กำลังทำงาน (ยังไม่รู้ผล)
  • Fulfilled — สำเร็จ (มีผลลัพธ์)
  • Rejected — ล้มเหลว (มี error)

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

4.2 ใช้ Promise — .then และ .catch

ส่วนใหญ่เราไม่ได้สร้าง Promise เอง — เราใช้ Promise ที่ function อื่นคืนมา (เช่น fetch)

💡 fetch คืออะไร?fetch(url) คือฟังก์ชันในตัวของเบราว์เซอร์ และ Node.js (stable ตั้งแต่ Node 18 LTS แล้ว ไม่ต้องเปิด flag พิเศษ) สำหรับ "ขอข้อมูลจาก server ผ่าน internet" คืนค่าเป็น Promise ที่ resolve เป็น Response object พอได้ response แล้วเรียก .json() (ซึ่งก็คืน Promise อีกตัว) เพื่อแปลงเป็น object ใช้งานได้

javascript
// (URL ตัวอย่างนี้ไม่มีจริง — ใน Lab จะใช้ URL จริงให้)
fetch("https://api.example.com/user")     // fetch คืน Promise
  .then(response => response.json())        // ถ้าสำเร็จ → ทำต่อ
  .then(data => console.log(data))           // ทำต่ออีก
  .catch(error => console.log("ผิดพลาด:", error));   // ถ้าล้มเหลว → จับ error

อธิบาย:

  • .then(fn) — "ถ้า Promise สำเร็จ ให้เรียก fn พร้อมผลลัพธ์"
  • .catch(fn) — "ถ้า Promise ล้มเหลว ให้เรียก fn พร้อม error"

4.3 Chain — ต่อ .then กันได้

จุดเด่นของ Promise — ต่อ .then กันเป็นทอด ๆ ได้ (แก้ callback hell):

javascript
// callback hell แบบเดิม → กลายเป็น chain แบน ๆ
loadUser(1)
  .then(user => loadOrders(user.id))
  .then(orders => loadOrderDetails(orders[0].id))
  .then(details => loadShipping(details.shippingId))
  .then(shipping => console.log(shipping))
  .catch(error => console.log("เกิดข้อผิดพลาด:", error));

เปรียบเทียบกับ callback hell ใน Part 3.2 — อันนี้แบน อ่านเป็นลำดับ และมี .catch เดียวจับ error ทุกขั้น

กฎของ chain: ถ้า .then คืนค่าอะไร — .then ถัดไปรับค่านั้น. ถ้าคืน Promise — มันรอ Promise นั้นเสร็จก่อน

4.4 สร้าง Promise เอง

บางครั้งเราต้องสร้าง Promise เอง (เช่นห่อ callback API เก่า):

javascript
function delay(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("เสร็จแล้ว!");      // resolve = สำเร็จ
    }, ms);
  });
}

delay(2000).then(result => console.log(result));
// (รอ 2 วินาที) → "เสร็จแล้ว!"

อธิบาย — new Promise((resolve, reject) => {...}):

  • function ที่ส่งเข้าไปรับ 2 อย่าง: resolve และ reject
  • เรียก resolve(value) = "Promise สำเร็จ ด้วยค่านี้"
  • เรียก reject(error) = "Promise ล้มเหลว ด้วย error นี้"

delay เป็นฟังก์ชันที่มีประโยชน์ — "หน่วงเวลา" แบบ async


Part 5: async / await — วิธีที่ดีที่สุด

Promise ดีกว่า callback มาก — แต่ ES2017 เพิ่ม async/await ที่ดียิ่งกว่า — มันทำให้โค้ด async "หน้าตาเหมือนโค้ด sync"

5.1 พื้นฐาน

javascript
async function loadUserData() {
  const response = await fetch("https://api.example.com/user");
  const data = await response.json();
  console.log(data);
}

อธิบาย:

  • async หน้า function — บอกว่า "function นี้เป็น async" (มันจะคืน Promise เสมอ)
  • await หน้า Promise — "รอ Promise นี้เสร็จ แล้วเอาผลลัพธ์ออกมา"

จุดมหัศจรรย์ — await ทำให้โค้ดที่จริง ๆ เป็น async อ่านเหมือน sync (บนลงล่าง ทีละบรรทัด) — แต่เบื้องหลังมันไม่ block!

5.2 เทียบ Promise chain กับ async/await

ดูโค้ดเดียวกัน 2 แบบ:

javascript
// แบบ Promise chain
function getUserOrders(userId) {
  return loadUser(userId)
    .then(user => loadOrders(user.id))
    .then(orders => {
      console.log(orders);
      return orders;
    });
}

// แบบ async/await — อ่านง่ายกว่ามาก
async function getUserOrders(userId) {
  const user = await loadUser(userId);
  const orders = await loadOrders(user.id);
  console.log(orders);
  return orders;
}

async/await อ่านเป็นลำดับ "ทำอันนี้ → รอ → ทำอันต่อไป" — เหมือนโค้ดธรรมดา

5.3 จัดการ Error ด้วย try/catch

ใน async/await — จับ error ด้วย try/catch (เหมือนโค้ด sync):

javascript
async function loadUserData() {
  try {
    const response = await fetch("https://api.example.com/user");
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.log("เกิดข้อผิดพลาด:", error.message);
    return null;          // ⚠️ ตัวอย่างเฉย ๆ — ดูคำเตือนด้านล่าง!
  }
}

try/catch จับ error ได้ทุกบรรทัดใน try — ไม่ว่าจะเป็น Promise ที่ reject หรือ error ที่ throw

🔴 คำเตือน — return null ใน catch คือ "anti-pattern กลืน error" (swallow-error):

โค้ดด้านบนเขียนแบบนี้เพื่อความง่าย แต่ในงานจริง อย่าทำ เพราะ caller แยกไม่ออกระหว่าง "โหลดสำเร็จแต่ user ไม่มีจริง ๆ" กับ "โหลดล้มเหลวเงียบ ๆ" → bug เงียบหายากเจอที่สุด

แนวทางที่ดีกว่า — เลือกอย่างใดอย่างหนึ่ง:

  1. โยน error ต่อ (rethrow) — ให้ caller ตัดสินใจเอง:

    javascript
    } catch (error) {
      console.log("เกิดข้อผิดพลาด:", error.message);
      throw error;       // ปล่อยให้ caller จัดการ — log อย่างเดียว ไม่กลืน
    }
  2. คืน Result type (object ที่บอกผลชัดเจน) — รูปแบบนี้พบใน Rust, Go, และโปรเจกต์ TypeScript จริงจัง:

    javascript
    } catch (error) {
      return { ok: false, error };
    }
    // caller: const r = await loadUserData();
    //         if (!r.ok) { ... } else { use r.value }

(เรื่อง error handling ลึก ๆ อยู่บทที่ 11)

5.4 async function คืน Promise เสมอ

javascript
async function getNumber() {
  return 42;
}

getNumber();                        // คืน Promise ไม่ใช่ 42!
getNumber().then(n => console.log(n));   // 42

แม้ดูเหมือน return เลข 42 — แต่ async function ห่อค่าด้วย Promise เสมอ → ต้องใช้ await หรือ .then ถึงจะได้ค่าจริง

5.5 Top-level await

ใน ES module (บทที่ 10) — ใช้ await นอก async function ได้เลย:

javascript
// ในไฟล์ .mjs หรือไฟล์ .js ที่โปรเจกต์มี "type": "module" ใน package.json
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);

💡 เงื่อนไข: top-level await ใช้ได้เฉพาะใน ES module เท่านั้น — ใน Node ต้องตั้ง "type": "module" ใน package.json (ไฟล์ตั้งค่าโปรเจกต์ — จะเรียนในบทที่ 13) หรือใช้นามสกุล .mjs (นามสกุลที่บอก Node ว่าไฟล์นี้เป็น ES Module) ในเบราว์เซอร์ต้องใส่ <script type="module"> ถ้าใช้ในสคริปต์ปกติ (CommonJS / <script>) จะ error ทันที


Part 6: ทำงาน Async หลายอย่างพร้อมกัน

นี่คือเรื่องสำคัญ — มือใหม่มักทำงาน async ทีละอย่างทั้งที่ทำพร้อมกันได้

6.1 ปัญหา — ทำทีละอันโดยไม่จำเป็น

javascript
// ❌ ช้า — ทำทีละอัน
async function loadAll() {
  const user = await loadUser();        // รอ 1 วินาที
  const posts = await loadPosts();       // รออีก 1 วินาที
  const comments = await loadComments(); // รออีก 1 วินาที
  // รวม 3 วินาที!
}

ถ้า 3 งานนี้ไม่เกี่ยวกัน (ไม่ต้องรออันก่อนหน้า) — การทำทีละอันคือการเสียเวลา

6.2 Promise.all — ทำพร้อมกัน รอให้ครบ

javascript
// ✅ เร็ว — ทำพร้อมกัน
async function loadAll() {
  const [user, posts, comments] = await Promise.all([
    loadUser(),
    loadPosts(),
    loadComments(),
  ]);
  // รวมแค่ ~1 วินาที! (ทำพร้อมกัน)
}

Promise.all([...]) — เริ่มทุก Promise พร้อมกัน แล้วรอจนทุกอันเสร็จ → คืน array ของผลลัพธ์

⚠️ Promise.all — ถ้ามีอันไหน reject → Promise.all reject ทันที (สำนวนเรียกว่า "fail fast" = หยุดทันทีเมื่อมีตัวใดตัวหนึ่ง error ไม่รอให้ตัวอื่นเสร็จ)

📘 อ่านตอนเขียน production — มือใหม่ข้ามได้:

ข้อพึงระวังสำคัญ: Promise อื่นที่ยังทำงานอยู่ ไม่ถูกยกเลิก — มันจะรันต่อในเบื้องหลังจนเสร็จ (หรือ fail) แค่ผลลัพธ์ของมันถูกทิ้งเฉย ๆ (สิ่งนี้เรียกว่า "fail-fast leak" — Promise.all บอก fail แล้วแต่ leak งานค้างไว้)

ในงานจริงนี่หมายถึง:

  • การขอข้อมูลผ่าน internet (network call) ที่ค้างอยู่ → ยังกินแบนด์วิดท์ต่อ
  • การ query ฐานข้อมูล → ยังกิน connection
  • การอ่าน/เขียนไฟล์ → ยังยึด file handle

ผลคือเสียโควต้า (quota) หรือทำให้การปิดโปรแกรม (shutdown) ค้าง

ถ้าต้องการยกเลิกจริง ๆ ต้องผูก AbortController เข้ากับแต่ละ Promise และเรียก controller.abort() ใน .catch():

javascript
const controller = new AbortController();
try {
  const [a, b] = await Promise.all([
    fetch(url1, { signal: controller.signal }),
    fetch(url2, { signal: controller.signal }),
  ]);
} catch (err) {
  controller.abort();   // ยกเลิก fetch ที่ยังค้าง
  // fetch ที่ถูก abort จะ throw AbortError ซึ่งถูก swallow ที่นี่ — เป็นพฤติกรรมที่ตั้งใจ
  throw err;
}

6.3 Promise.allSettled — ทำพร้อมกัน รอครบ ไม่ว่าสำเร็จหรือไม่

ถ้าอยากให้รอครบทุกอัน ไม่ว่าอันไหนจะ fail — ใช้ Promise.allSettled (settled = "จบแล้ว ไม่ว่าผลจะออกมาทางไหน" — สำเร็จก็ได้ ล้มเหลวก็ได้ ขอแค่จบ):

💡 เวอร์ชัน: Promise.allSettled เป็นมาตรฐาน ES2020 — รองรับใน Node 12.9+ และเบราว์เซอร์สมัยใหม่ทุกตัว (ใช้ได้ทั่วไปในปัจจุบัน)

javascript
const results = await Promise.allSettled([
  loadUser(),
  loadPosts(),      // สมมติอันนี้ fail
  loadComments(),
]);

// results = [
//   { status: "fulfilled", value: ... },
//   { status: "rejected", reason: ... },
//   { status: "fulfilled", value: ... },
// ]

แต่ละผลลัพธ์บอก status ว่าสำเร็จหรือไม่ — เหมาะกับงานที่ "บางอันพังก็ไม่เป็นไร"

6.4 Promise.race — เอาอันที่เสร็จก่อน

javascript
const fastest = await Promise.race([
  fetch("https://server1.com/data"),
  fetch("https://server2.com/data"),
]);
// ได้ผลจาก server ที่ตอบเร็วกว่า

ใช้ทำ timeout — ถ้างานช้าเกินกำหนด ให้ถือว่า fail:

javascript
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error("หมดเวลา")), ms)
  );
  return Promise.race([promise, timeout]);
}

await withTimeout(fetch(url), 5000);    // ถ้าเกิน 5 วินาที → error

💡 หมายเหตุ: fetch ที่ "แพ้" ใน Promise.race ยังคงทำงานต่อในเบื้องหลังจนเสร็จ — ถ้าต้องการยกเลิกจริง ๆ ใช้ AbortSignal.timeout() แทน (ดู Part 8.2)

6.5 สรุป Promise combinators (ฟังก์ชันที่รวม Promise หลายตัว)

combinator = function ที่รับ Promise หลายตัวเข้าไปแล้วคืน Promise ตัวใหม่ที่รวมผลลัพธ์

ทำอะไรเวอร์ชัน
Promise.allทุกอันเสร็จ — ถ้าอันไหน fail → fail หมดES2015
Promise.allSettledรอครบทุกอัน ไม่ว่าสำเร็จหรือ failES2020 (Node 12.9+)
Promise.raceเอาอันที่เสร็จก่อน (สำเร็จหรือ fail ก็ได้)ES2015
Promise.anyเอาอันแรกที่ สำเร็จ — ถ้าทุกอัน fail จะโยน AggregateErrorES2021 (Node 15+)

6.6 Promise.any — รายละเอียดและ AggregateError

Promise.any คล้าย Promise.race แต่ ignore ทุก rejection จนกว่าจะได้ตัวที่สำเร็จ:

javascript
const fastest = await Promise.any([
  fetch("https://server1.com/data"),   // อาจ fail
  fetch("https://server2.com/data"),   // อาจ fail
  fetch("https://server3.com/data"),   // สำเร็จ → ได้อันนี้
]);

ข้อพึงระวังสำคัญ: ถ้า ทุกอัน rejectPromise.any จะโยน AggregateError (ไม่ใช่ Error ปกติ) — AggregateError คือ error ชนิดพิเศษที่ "รวมหลาย error เป็นชุดเดียว" มี property .errors เป็น array ของเหตุผลที่แต่ละ Promise fail:

javascript
try {
  const data = await Promise.any([
    fetch(url1),
    fetch(url2),
    fetch(url3),
  ]);
} catch (err) {
  if (err instanceof AggregateError) {
    console.log("ทุกอัน fail:");
    err.errors.forEach((e, i) => console.log(`  [${i}]`, e.message));
  }
}

→ อย่าใช้ err.message เฉย ๆ เพราะ AggregateError message ไม่บอก detail ของแต่ละ error


Part 7: async ใน Loop

7.1 ⚠️ forEach + async = กับดักคลาสสิก

จากบทที่ 04 — forEach ใช้กับ await ไม่ได้ตามที่คาด นี่เป็นหนึ่งใน "footgun" (กับดักที่เกิดจากการใช้งานผิดโดยไม่ตั้งใจ) ที่มือใหม่เจอบ่อยที่สุด:

javascript
// ❌ ผิด — forEach ไม่รอ await
items.forEach(async (item) => {
  await process(item);
});
console.log("เสร็จ");      // พิมพ์ทันที — ไม่รอ process จริง ๆ

ทำไม? — forEach เหมือนบอกลูกทีมให้ทำงาน แต่ไม่รอฟังผลลัพธ์ก่อนเดินหน้าต่อ — มันเรียก callback แล้วทิ้งค่าที่คืนมา (รวมทั้ง Promise ที่ async function คืนให้) → ไม่มีใครรอ Promise พวกนั้น

ใช้ for...of + await แทนเสมอเมื่อต้องการรอจริง (ดูข้อ 7.2)

7.2 ใช้ for...of แทน

javascript
// ✅ ถูก — for...of รอ await จริง ๆ
for (const item of items) {
  await process(item);     // รอทีละตัว
}
console.log("เสร็จ");      // พิมพ์หลังทุกตัวเสร็จจริง

for...of + await = ทำ "ทีละตัว" (ตัวก่อนเสร็จค่อยทำตัวถัดไป) — ใช้เมื่อลำดับสำคัญ

7.3 ถ้าอยากทำพร้อมกัน — map + Promise.all

javascript
// ทำทุกตัวพร้อมกัน
await Promise.all(items.map(item => process(item)));

→ จำ: ลำดับสำคัญ → for...of + await / ไม่สำคัญ ทำพร้อมกันได้ → map + Promise.all


Part 8: AbortController — ยกเลิกงาน async

บางครั้งเราอยาก "ยกเลิก" งาน async ที่กำลังทำ (เช่น ผู้ใช้ออกจากหน้าก่อนโหลดเสร็จ):

javascript
const controller = new AbortController();

fetch("https://api.example.com/data", { signal: controller.signal })
  .then(response => response.json())
  .catch(error => {
    if (error.name === "AbortError") {
      console.log("ถูกยกเลิก");
    }
  });

// ยกเลิกหลัง 5 วินาที
setTimeout(() => controller.abort(), 5000);

AbortController ใช้ยกเลิก fetch, event listener, หรืองาน async อื่น ๆ

8.1 รายละเอียดที่ต้องรู้

  • เมื่อ fetch ถูก abort — Promise จะ reject ด้วย DOMException ที่ .name === "AbortError" ไม่ใช่ Error ปกติ (DOMException = error ชนิดพิเศษของเบราว์เซอร์ มี property .name บอกชนิดย่อย) ต้องเช็ค err.name === "AbortError" เพื่อแยกออกจาก network error อื่น
  • เรียก abort() หลัง Promise resolve แล้ว — ไม่มีผล (เงียบ ๆ ผ่านไป)
  • ใช้ try/finally เพื่อ cleanup signal/timer ที่ผูกกัน
  • AbortController + fetch เป็นรูปแบบมาตรฐาน สำหรับ "fetch ที่ยกเลิกได้" ทุกตัวในงานจริง — ใช้ทุกครั้งที่ทำ wrapper รอบ fetch (เช่น API client, retry, timeout)

8.2 API ใหม่ที่ควรรู้

(last reviewed: 2026-06 — ตรวจ browser/Node version compatibility ที่ caniuse.com ก่อนใช้)

AbortSignal.timeout(ms) (Node 17.3+, เบราว์เซอร์สมัยใหม่) — สร้าง signal ที่ abort เองหลังเวลาที่กำหนด สั้นกว่าเขียน setTimeout เอง:

javascript
try {
  const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
  const data = await res.json();
} catch (err) {
  // หมายเหตุ: AbortSignal.timeout() reject ด้วย .name === "TimeoutError"
  //           ส่วน controller.abort() reject ด้วย .name === "AbortError"
  if (err.name === "TimeoutError" || err.name === "AbortError") {
    console.log("หมดเวลาหรือถูกยกเลิก");
  }
}

AbortSignal.any([sig1, sig2, ...]) (รองรับใน Node รุ่นใหม่และเบราว์เซอร์สมัยใหม่ — กำหนดโดย WHATWG ไม่ใช่ TC39/ES2024 — ตรวจสอบเวอร์ชันล่าสุดที่ caniuse.com ก่อนใช้) — รวมหลาย signal เข้าด้วยกัน abort ทันทีที่ตัวไหนตัวหนึ่ง abort ใช้รวม "ผู้ใช้กดยกเลิก" + "หมดเวลา":

javascript
const userSignal = controller.signal;
const timeoutSignal = AbortSignal.timeout(10000);
const combined = AbortSignal.any([userSignal, timeoutSignal]);

await fetch(url, { signal: combined });

⚠️ ถ้า Node เวอร์ชันเก่ากว่า 20 อาจไม่มี AbortSignal.any และเก่ากว่า 17.3 จะไม่มี AbortSignal.timeout ด้วย — ตรวจ Node version และ browser support ที่ caniuse.com ก่อนใช้ใน production


Part 9: รูปแบบที่ใช้บ่อยในงานจริง

9.1 Sleep / Delay

JavaScript ไม่มีฟังก์ชัน "sleep" ในตัว แต่เราสร้างเองง่าย ๆ ด้วย Promise ที่ resolve หลัง setTimeout — แล้ว await มันเพื่อหน่วงเวลาในฟังก์ชัน async:

javascript
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

async function demo() {
  console.log("เริ่ม");
  await sleep(1000);            // รอ 1 วินาที
  console.log("ผ่านไป 1 วินาที");
}

9.2 Retry — ลองใหม่เมื่อ fail

งาน async อย่างยิง API อาจล้มเหลวชั่วคราว (network สะดุด) แพตเทิร์น retry คือลองใหม่อัตโนมัติหลาย ครั้งก่อนยอมแพ้ — มักรอนานขึ้นเรื่อย ๆ ในแต่ละครั้ง (backoff):

javascript
async function retry(fn, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();              // สำเร็จ → คืนผลเลย
    } catch (error) {
      console.log(`ครั้งที่ ${attempt} ล้มเหลว`);
      if (attempt === maxAttempts) throw error;   // ครั้งสุดท้ายแล้ว → โยน error
      await sleep(1000 * attempt);     // รอก่อนลองใหม่ (รอนานขึ้นเรื่อย ๆ)
    }
  }
}

// ใช้
const data = await retry(() => fetch(url).then(r => r.json()));

📘 อ่านเฉพาะตอนจะใช้จริงใน production — มือใหม่อ่าน overview พอ:

โค้ดด้านบนเขียนแบบเรียบ ๆ เพื่อให้เข้าใจ idea หลัก แต่ใน production ต้องระวัง 4 จุด:

  1. ใช้ "exponential backoff" + "jitter" (รอเพิ่มแบบทวีคูณ + สุ่มเขย่าเวลา) — ถ้ารอแบบ linear (1s, 2s, 3s) client ทุกตัวที่ fail พร้อมกันจะ retry พร้อมกันด้วย ผลคือ service ที่เพิ่งฟื้นตัวโดน client ทุกตัวถล่มซ้ำพร้อมกัน ทำให้โหลดหนักซ้ำและอาจล่มอีกครั้ง — สำนวนเรียกอาการนี้ว่า "thundering herd problem" (แปลตรงตัวว่า "ฝูงสัตว์แตกตื่นวิ่งพร้อมกัน" — เปรียบ client จำนวนมากแห่กัน retry พร้อมกันเหมือนฝูงสัตว์ตื่นตกใจวิ่งพร้อมกัน) ทางแก้คือ "เขย่าเวลาสุ่ม" (jitter) ทำให้แต่ละ client retry คนละจังหวะ ไม่ชนกัน
  2. แยก error ที่ retry ได้ vs ไม่ได้ (retryable vs non-retryable)HTTP 4xx ส่วนใหญ่ (400, 401, 403, 404) ไม่ควร retry เพราะจะไม่มีวันสำเร็จ (ขอผิดก็ขอผิดอีก) ยกเว้น 408 (timeout) และ 429 (rate limit). 5xx และ network error retry ได้
  3. ผูก AbortSignal — ผู้ใช้กดยกเลิกตอน retry ค้างต้องหยุดได้
  4. จำกัดเวลารวมสูงสุด (max total delay / budget) — กันไม่ให้ retry รวมกันยาวเกิน budget เช่น 30 วินาที

ตัวอย่างโครงร่าง production retry (อ่าน overview พอ ไม่ต้องเข้าใจทุกบรรทัด):

javascript
async function retry(fn, { maxAttempts = 3, baseMs = 200, maxMs = 5000, signal, isRetryable } = {}) {
  // maxAttempts = ลองกี่ครั้ง, baseMs = เวลารอตั้งต้น, maxMs = เพดานเวลารอ
  // signal = AbortSignal สำหรับยกเลิก, isRetryable = ฟังก์ชันตัดสินว่า error นี้ retry ได้ไหม
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn({ signal });
    } catch (err) {
      if (signal?.aborted) throw err;                       // ผู้ใช้ยกเลิก → หยุดทันที
      if (attempt === maxAttempts) throw err;               // ครั้งสุดท้ายแล้ว
      if (isRetryable && !isRetryable(err)) throw err;      // error ที่ retry ไปก็ไม่ช่วย
      const expo = Math.min(maxMs, baseMs * 2 ** (attempt - 1));  // exponential cap
      const jitter = Math.random() * expo;                  // "full jitter" = สุ่ม 0 ถึง expo (retry อาจเร็วมาก แต่กระจายออกไม่ pile-up)
      await sleep(jitter);
    }
  }
}

9.3 โหลดข้อมูลพร้อม loading state

แพตเทิร์นที่เจอในทุก UI: ติดตามสถานะ 3 อย่างตอนโหลดข้อมูล — loading (กำลังโหลด), data (สำเร็จ), error (ล้มเหลว) โดยใช้ try/catch/finally จัดการให้ครบทุกกรณี:

javascript
async function loadWithState() {
  let state = { loading: true, data: null, error: null };

  try {
    const response = await fetch(url);
    state.data = await response.json();
  } catch (error) {
    state.error = error;
  } finally {
    state.loading = false;
  }

  return state;
}

⚠️ โค้ดข้างบนเป็น "ภาพนิ่งตอนจบ" (snapshot) เท่านั้น — ไม่ใช่ loading state ที่ UI เห็น "ระหว่างกาล" จริง

เพราะ function เป็น async ผู้เรียก (caller) ได้ค่าเฉพาะตอน Promise resolve (คือตอนเสร็จแล้ว) จึงไม่มีทาง "เห็น" loading: true ระหว่างกาลเลย — UI จะไม่เคยเห็น spinner

ถ้าต้องการให้ UI ตอบสนองตอนกำลังโหลด ต้องใช้รูปแบบที่ "ตอบสนองอัตโนมัติ" (reactive — เปลี่ยนตามอัตโนมัติเมื่อ state เปลี่ยน) + "สมัครรับการเปลี่ยนแปลง" (subscribe — ลงทะเบียนรับ event เมื่อค่าเปลี่ยน) เช่น:

  • ใน React/Vue/Angular ใช้ state hook / signal ของเฟรมเวิร์ก (useState, ref, signal) และตั้ง loading=true ก่อน fetch แล้ว loading=false ใน finally
  • หรือใช้ callback แบบ onStateChange(state) ที่ถูกเรียกหลายครั้ง
  • หรือใช้ store library (Redux, Zustand, Pinia) ที่ UI ลงทะเบียนรับการเปลี่ยนแปลงไว้

Part 10: ข้อผิดพลาดที่พบบ่อย

10.1 ลืม await

ข้อผิดพลาดอันดับหนึ่งของมือใหม่ — ลืมใส่ await ทำให้ได้ Promise (ตัวห่อ) แทนข้อมูลจริง พอเอาไปใช้เลยได้ undefined:

javascript
async function getData() {
  const data = fetch(url);        // ❌ ลืม await — data เป็น Promise ไม่ใช่ข้อมูล
  console.log(data.name);          // undefined
}

→ แก้: const data = await fetch(url)

10.2 ลืมจับ error

ถ้าฟังก์ชัน async โยน error แล้วไม่มีใครจับ (try/catch หรือ .catch()) จะเกิด "unhandled promise rejection" ("error ที่ไม่ถูกจับ" — Promise พังแล้วไม่มีใครรับฟัง) — error เงียบหายหรือทำให้โปรแกรมพัง:

javascript
async function risky() {
  throw new Error("พัง");
}

risky();        // ❌ error ไม่ถูกจับ → "unhandled promise rejection"

→ แก้: ใช้ try/catch หรือ .catch()

javascript
risky().catch(error => console.log(error));

10.3 ใช้ await ใน loop ทั้งที่ทำพร้อมกันได้

await ใน loop ทำให้งานทำ "ทีละอัน" (รอตัวก่อนเสร็จค่อยเริ่มตัวถัดไป) — ถ้างานไม่ขึ้นต่อกันและลำดับไม่สำคัญ ควรยิงพร้อมกันด้วย Promise.all ซึ่งเร็วกว่ามาก:

javascript
// ❌ ช้า
for (const url of urls) {
  await fetch(url);
}

// ✅ เร็ว (ถ้าลำดับไม่สำคัญ)
await Promise.all(urls.map(url => fetch(url)));

Part 11: Lab — ลงมือทำ

Lab 1: Sleep

ฝึกใช้ sleep ที่สร้างเองร่วมกับ await ใน loop เพื่อทำนับถอยหลังที่หน่วงทีละวินาที — เห็นการควบคุมจังหวะเวลาด้วย async:

javascript
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

async function countdown() {
  for (let i = 3; i > 0; i--) {
    console.log(i);
    await sleep(1000);
  }
  console.log("เริ่ม!");
}

countdown();

Lab 2: เทียบ sequential vs parallel

ฝึกวัดเวลาจริงเพื่อเห็นความต่างระหว่างทำทีละอัน (await ต่อกัน ~3 วินาที) กับทำพร้อมกัน (Promise.all ~1 วินาที) — บทเรียนสำคัญเรื่อง performance ของ async:

javascript
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const task = (name, ms) => sleep(ms).then(() => name);

// ทีละอัน — ช้า
async function sequential() {
  console.time("sequential");
  await task("a", 1000);
  await task("b", 1000);
  await task("c", 1000);
  console.timeEnd("sequential");      // ~3000ms
}

// พร้อมกัน — เร็ว
async function parallel() {
  console.time("parallel");
  await Promise.all([
    task("a", 1000),
    task("b", 1000),
    task("c", 1000),
  ]);
  console.timeEnd("parallel");        // ~1000ms
}

Lab 3: fetch จริง (ต้องมี internet)

ฝึกยิง API จริงด้วย fetch พร้อมจัดการครบทุกขั้น — เช็ค response.ok, แปลง JSON, และจับ error ด้วย try/catch:

javascript
async function getRandomUser() {
  try {
    const response = await fetch("https://randomuser.me/api/");
    if (!response.ok) throw new Error("โหลดไม่สำเร็จ");
    const data = await response.json();
    const user = data.results[0];
    console.log(`${user.name.first} ${user.name.last}`);
  } catch (error) {
    console.log("ผิดพลาด:", error.message);
  }
}

getRandomUser().catch(console.error);
// หมายเหตุ: ถ้าต้องการใช้ top-level await ต้องอยู่ใน ES module (type: "module")

Lab 4: Retry with timeout

ฝึกใส่ timeout ให้ fetch ผ่าน AbortSignal.timeout() พร้อมจัดการ retry — เป็นรูปแบบมาตรฐานในงานจริง:

javascript
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

async function fetchWithRetry(url, maxAttempts = 3) {
  for (let i = 1; i <= maxAttempts; i++) {
    try {
      // AbortSignal.timeout(5000) → ถ้า fetch เกิน 5 วินาที จะถูกยกเลิก
      // หมายเหตุ: timeout จะโยน error.name === "TimeoutError" (ไม่ใช่ "AbortError")
      const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      console.log(`ครั้งที่ ${i}: ${error.message}`);
      if (i === maxAttempts) throw error;
      // หมายเหตุ: Lab นี้ retry ทุก error เพื่อความง่าย
      // ในงานจริงควรแยก error ที่ retry ได้ (5xx, network) กับที่ retry ไม่ได้ (4xx) — ดู Part 9.2
      await sleep(1000 * i);
    }
  }
}

Part 12: Checkpoint

  1. ทำไม JavaScript ต้องมี async? อะไรคือ "blocking"?
  2. Event Loop ทำงานยังไง? (อธิบายด้วยตัวอย่าง setTimeout)
  3. setTimeout(fn, 0) ทำงานทันทีไหม? ทำไม?
  4. Callback Hell คืออะไร? เกิดจากอะไร?
  5. Promise มี 3 สถานะอะไร?
  6. .then กับ .catch ทำอะไร?
  7. async/await ดีกว่า Promise chain ยังไง?
  8. try/catch ใน async/await จับ error อะไรได้บ้าง?
  9. Promise.all ต่าง Promise.allSettled ยังไง?
  10. ทำไม forEach ใช้กับ await ไม่ได้? ใช้อะไรแทน?

Part 13: สรุปบทนี้

  • Async = "ตั้งงานไว้ ทำอื่นต่อ พอเสร็จค่อยกลับมา" — ไม่ block
  • Event Loop = กลไกที่จัดการ async — ทำงาน sync ก่อน แล้วค่อยดึง callback จากคิว
  • setTimeout(fn, 0) ไม่ใช่ "ทันที" — มันคือ "หลังงาน sync เสร็จ"
  • วิวัฒนาการ: callback (เกิด callback hell) → Promise (chain ได้) → async/await (ดีที่สุด)
  • Promise มี 3 สถานะ: pending → fulfilled / rejected
  • async/await — เขียน async ให้ดูเหมือน sync — จับ error ด้วย try/catch
  • Promise.all = ทำพร้อมกัน รอครบ / allSettled = รอครบไม่ว่าสำเร็จหรือไม่
  • for...of + await = ทีละตัว / map + Promise.all = พร้อมกัน
  • อย่าลืม await, อย่าลืมจับ error

บทต่อไป — Modules: แบ่งโค้ดเป็นไฟล์

ไปต่อ (หัวข้อขั้นสูงที่ไม่ได้ครอบในบทนี้)

ถ้าอยากลึกกว่านี้ในระดับ senior — ศึกษาเสริม (ไม่จำเป็นสำหรับมือใหม่):

  • Async iterator + for await...of (วน loop ผ่าน stream ของ Promise) — เช่นอ่านข้อมูลทีละก้อนจาก server
  • Async generator (async function*) (ฟังก์ชันที่ "ผลิตค่า async ทีละตัว") — เหมือน generator ปกติแต่ผลิต Promise
  • Streams API + backpressure (ระบบไหลข้อมูล + การกันท่ออุด) — รับมือกับฝั่งผลิตเร็วกว่าฝั่งบริโภค
  • Web Worker / Worker thread (Node) — รัน JS หลาย thread จริง ๆ อยู่นอก event loop หลัก
  • SharedArrayBuffer + Atomics — หน่วยความจำที่ worker หลายตัวใช้ร่วมกัน (ใช้ระวัง — race condition ได้ง่าย)
  • Error cause chain (new Error(msg, { cause })) — ส่งต่อสาเหตุต้นทางของ error เป็นทอด ๆ
  • Structured concurrency patterns ("ระบบงาน async ที่จัดการอายุงานชัดเจน" — เช่น nursery / task group ใน Python/Kotlin)
  • Node-specific: process.nextTick vs queueMicrotask, setImmediate vs setTimeout(0) — รายละเอียดคิวที่ Node เพิ่มมาเฉพาะ

แหล่งอ้างอิงหลัก: MDN — Concurrency model and Event Loop, Node.js docs — Event Loop, V8 blog


← บทที่ 08 | สารบัญ | บทที่ 10: Modules →


🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-12