Promise ใน JavaScript คืออะไร (พร้อมตัวอย่าง async/await)

Promise ใน JavaScript คือ object ที่ใช้แทน ผลลัพธ์ของงานที่ยังทำไม่เสร็จในตอนนี้ แต่จะเสร็จในอนาคต เช่น การดึงข้อมูลจาก API, อ่านไฟล์ หรือรอเวลาผ่านไปตามที่กำหนด บทความนี้จะอธิบาย Promise คืออะไรแบบเข้าใจง่าย พร้อมตัวอย่าง async/await ซึ่งเป็นวิธีเขียนที่นิยมที่สุดในปัจจุบัน

ถ้าเคยเห็นโค้ด JavaScript ที่มีคำว่า .then(), .catch() หรือ await ปนอยู่ในนั้นคือกำลังเจอกับ Promise อยู่ เป็นแนวคิดที่หลีกเลี่ยงไม่ได้เลยสำหรับใครก็ตามที่เขียน JavaScript ไม่ว่าจะทำเว็บฝั่ง frontend หรือ backend ด้วย Node.js ก็ตาม

ทำไมต้องมี Promise

JavaScript ทำงานแบบ asynchronous (ไม่รอ) เป็นค่าเริ่มต้น เมื่อสั่งงานบางอย่างที่ใช้เวลา เช่น ดึงข้อมูลจากเซิร์ฟเวอร์ โค้ดจะไม่หยุดรอผลลัพธ์ตรงนั้น แต่จะทำงานบรรทัดถัดไปต่อทันที แล้วค่อยกลับมาจัดการผลลัพธ์เมื่องานนั้นเสร็จจริง

ก่อนมี Promise วิธีจัดการงานแบบนี้คือใช้ callback function ส่งฟังก์ชันเข้าไปให้เรียกกลับเมื่องานเสร็จ ปัญหาคือถ้ามีงาน asynchronous ต่อเนื่องกันหลายขั้นตอน (เช่น ดึงข้อมูล A แล้วเอาไปดึงข้อมูล B แล้วเอาไปดึงข้อมูล C) โค้ดจะซ้อนกันลึกขึ้นเรื่อยๆ จนอ่านยากมาก เรียกปัญหานี้ว่า callback hell

Promise เข้ามาแก้ปัญหานี้ ทำให้เขียนงาน asynchronous ต่อเนื่องกันได้อย่างเป็นระเบียบ อ่านง่ายขึ้นมาก

สถานะของ Promise มี 3 แบบ

Promise มีสถานะอยู่เสมอ 1 ใน 3 แบบนี้

  • pending — ยังทำงานไม่เสร็จ กำลังรอผลลัพธ์
  • fulfilled — ทำงานเสร็จสำเร็จ ได้ผลลัพธ์ตามที่ต้องการ
  • rejected — ทำงานล้มเหลว เกิดข้อผิดพลาดระหว่างทาง

Promise เริ่มต้นที่สถานะ pending เสมอ แล้วจะเปลี่ยนเป็น fulfilled หรือ rejected เมื่องานเสร็จสิ้น เปลี่ยนได้แค่ครั้งเดียวเท่านั้น ไม่สามารถย้อนกลับไป pending อีกได้หลังเปลี่ยนสถานะแล้ว ลักษณะนี้ทำให้ Promise คาดเดาพฤติกรรมได้ง่ายกว่า callback แบบเดิมมาก เพราะรู้แน่ชัดว่าจบแล้วจบเลย ไม่มีทางถูกเรียกซ้ำสองครั้งโดยไม่ตั้งใจ

สร้างและใช้ Promise ด้วย then/catch

const getUserData = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve({ name: "สมชาย", age: 25 });
  } else {
    reject("ดึงข้อมูลไม่สำเร็จ");
  }
});

getUserData
  .then((data) => console.log(data))
  .catch((error) => console.log(error));

resolve เรียกเมื่อทำงานสำเร็จ ส่งค่าผลลัพธ์ไปให้ .then() รับต่อ ส่วน reject เรียกเมื่อเกิดข้อผิดพลาด ส่งไปให้ .catch() รับต่อแทน ในโค้ดจริงมักไม่ต้องสร้าง Promise เองแบบนี้บ่อยนัก เพราะฟังก์ชันมาตรฐานของ JavaScript อย่าง fetch() คืน Promise ให้อยู่แล้วโดยอัตโนมัติ

fetch("/api/user")
  .then((res) => res.json())
  .then((data) => console.log(data))
  .catch((error) => console.log("เกิดข้อผิดพลาด:", error));

async/await คืออะไร

async/await คือวิธีเขียนงาน asynchronous ที่สร้างขึ้นมาบน Promise อีกที ทำให้โค้ดที่ใช้ Promise อ่านเหมือนโค้ดปกติที่ทำงานทีละบรรทัด แม้เบื้องหลังยังคงเป็นการทำงานแบบ asynchronous เหมือนเดิม

async function getUser() {
  try {
    const res = await fetch("/api/user");
    const data = await res.json();
    console.log(data);
  } catch (error) {
    console.log("เกิดข้อผิดพลาด:", error);
  }
}

ฟังก์ชันที่ใช้ await ข้างในต้องประกาศด้วยคำว่า async นำหน้าเสมอ คำสั่ง await จะ “รอ” จนกว่า Promise ตัวนั้นจะเสร็จก่อนค่อยไปบรรทัดถัดไป ทำให้โค้ดอ่านเป็นลำดับขั้นตอนตรงไปตรงมา ไม่ต้องซ้อน .then() หลายชั้นเหมือนวิธีแรก

เทียบ then/catch กับ async/await

ทั้งสองแบบทำงานเหมือนกันทุกประการ เป็นแค่คนละ “รูปแบบการเขียน” (syntax) เท่านั้น เลือกใช้ตามความถนัดได้ แต่ async/await ได้รับความนิยมมากกว่าในปัจจุบัน เพราะอ่านง่ายกว่าเมื่อมีงาน asynchronous ต่อเนื่องกันหลายขั้นตอน

โค้ดสองชุดด้านล่างทำงานเหมือนกันทุกประการ

function getData() {
  return fetch("/api/a")
    .then((res) => res.json())
    .then((a) => fetch(`/api/b/${a.id}`))
    .then((res) => res.json());
}
async function getData() {
  const resA = await fetch("/api/a");
  const a = await resA.json();
  const resB = await fetch(`/api/b/${a.id}`);
  return await resB.json();
}

เห็นได้ชัดว่าเวอร์ชัน async/await อ่านง่ายกว่ามาก โดยเฉพาะเมื่อมีขั้นตอนต่อเนื่องกันหลายชั้น ยิ่งจำนวนขั้นตอนมากขึ้นเท่าไหร่ ความต่างของความอ่านง่ายก็ยิ่งเห็นชัดมากขึ้นตามไปด้วย

error handling ด้วย try/catch

ใน async/await การจัดการ error ทำผ่าน try/catch แบบเดียวกับโค้ด synchronous ทั่วไป ครอบโค้ดที่มี await ไว้ใน try แล้วดัก error ที่ catch ถ้า Promise ตัวไหนถูก reject ระหว่างทาง โค้ดจะกระโดดไปที่ catch ทันที เหมือนกับการ throw error ปกติทุกประการ ข้อดีของวิธีนี้คือใช้ pattern เดียวกับการจัดการ error ในโค้ดปกติทั่วไปที่คุ้นเคยอยู่แล้ว ไม่ต้องเรียนรู้วิธีคิดใหม่แยกต่างหากสำหรับงาน asynchronous โดยเฉพาะ

Promise.all ใช้ตอนไหน

ถ้าต้องการรอหลาย Promise ทำงานพร้อมกันแทนที่จะรอทีละตัวตามลำดับ ใช้ Promise.all() ได้

async function getAllData() {
  const [users, posts] = await Promise.all([
    fetch("/api/users").then((r) => r.json()),
    fetch("/api/posts").then((r) => r.json()),
  ]);
  console.log(users, posts);
}

Promise.all() รอทุก Promise ใน array เสร็จพร้อมกัน เร็วกว่าการ await ทีละตัวตามลำดับมาก เพราะทั้งสอง request ยิงออกไปพร้อมกันตั้งแต่แรก แทนที่จะรอ request แรกเสร็จก่อนค่อยเริ่ม request ที่สอง ข้อควรระวังคือถ้า Promise ตัวใดตัวหนึ่งถูก reject Promise.all() จะ reject ทันทีทั้งชุด แม้ตัวอื่นจะยังทำงานสำเร็จอยู่ก็ตาม ความเร็วที่ต่างกันนี้ยิ่งเห็นชัดเมื่อแต่ละ request ใช้เวลานานหลายวินาที เพราะการรอทีละตัวจะรวมเวลาทั้งหมดเข้าด้วยกัน ในขณะที่รอพร้อมกันใช้เวลาเท่ากับ request ที่ช้าที่สุดเพียงตัวเดียว

ตัวอย่างที่พบบ่อย: รอเวลาด้วย Promise

อีกตัวอย่างที่ช่วยให้เห็นภาพ Promise ชัดเจนคือการสร้างฟังก์ชัน “หน่วงเวลา” ด้วยตัวเอง เพราะ JavaScript ไม่มีฟังก์ชัน sleep() แบบภาษาอื่นมาให้ในตัว

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function run() {
  console.log("เริ่มทำงาน");
  await delay(2000);
  console.log("ผ่านไป 2 วินาทีแล้ว");
}

ฟังก์ชัน delay สร้าง Promise ที่จะ resolve เองอัตโนมัติหลังจากเวลาที่กำหนด (หน่วยเป็นมิลลิวินาที) ผ่านไป เมื่อใช้ร่วมกับ await ในฟังก์ชัน async จะทำให้โค้ดหยุดรอตรงจุดนั้นจริงๆ ก่อนไปทำงานบรรทัดถัดไป มีประโยชน์มากเวลาต้องการหน่วงเวลาระหว่างการทำงานหลายขั้นตอน เช่น ทดสอบ UI หรือหน่วงเวลาก่อนลองส่ง request ใหม่หลัง request แรกล้มเหลว เป็น pattern ที่ใช้บ่อยมากในการเขียนโค้ดทดสอบ (test) ด้วยเช่นกัน

ข้อผิดพลาดที่มือใหม่เจอบ่อย

ลืมว่า Promise.all หยุดทั้งชุดถ้ามีตัวใดตัวหนึ่งพัง — ถ้าต้องการให้งานอื่นทำต่อแม้บางตัวจะล้มเหลว ต้องใช้ Promise.allSettled() แทน ไม่ใช่ Promise.all()

เขียน await นอกฟังก์ชัน asyncawait ใช้ได้เฉพาะภายในฟังก์ชันที่ประกาศด้วย async เท่านั้น (ยกเว้น top-level await ในบาง environment ที่รองรับเป็นพิเศษ) ถ้าเขียนผิดที่จะเจอ syntax error ทันที

สับสนระหว่าง Promise ที่ยังไม่ resolve กับค่าที่ resolve แล้ว — ตัวแปรที่เก็บ Promise ไว้โดยไม่ได้ await หรือ .then() จะยังเป็น Promise object อยู่ ไม่ใช่ค่าจริงที่ต้องการ ต้องดึงค่าออกมาก่อนเสมอถึงจะนำไปใช้งานต่อได้ถูกต้อง ลองใช้ console.log() ดูตัวแปรนั้นก่อนถ้าไม่แน่ใจว่าเป็น Promise หรือค่าจริงกันแน่

คำถามที่พบบ่อย

ต้องเข้าใจ Promise ก่อนถึงจะใช้ async/await ได้ไหม ควรเข้าใจภาพรวมพื้นฐานไว้ก่อน เพราะ async/await ทำงานอยู่บน Promise อยู่ดี แต่ไม่จำเป็นต้องเชี่ยวชาญการสร้าง Promise เองด้วยมือ เพราะในงานจริงมักใช้ Promise ที่ฟังก์ชันมาตรฐานคืนมาให้อยู่แล้ว (เช่น fetch) มากกว่าสร้างเอง

ลืมใส่ await จะเกิดอะไรขึ้น โค้ดจะไม่ error ทันที แต่ตัวแปรที่ควรได้ผลลัพธ์จริงจะกลายเป็น Promise object แทนค่าที่ต้องการ เป็นข้อผิดพลาดที่มือใหม่เจอบ่อยมาก อาการมักเป็นค่าที่ได้ผิดประเภทหรือ undefined เวลาพยายามใช้งานค่านั้นต่อ

Promise.all กับ Promise.allSettled ต่างกันยังไง Promise.all() จะ reject ทั้งชุดทันทีถ้ามีตัวใดตัวหนึ่งล้มเหลว ส่วน Promise.allSettled() รอทุกตัวจนจบเสมอ ไม่ว่าจะสำเร็จหรือล้มเหลว แล้วคืนผลลัพธ์ของทุกตัวมาให้ดูทีเดียว เหมาะกับกรณีที่ต้องการรู้ผลของทุก request แม้บางตัวจะพังก็ตาม

async function คืนค่าอะไรกันแน่ async function คืนค่าเป็น Promise เสมอ แม้ข้างในจะ return ค่าธรรมดาก็ตาม JavaScript จะห่อค่านั้นเป็น Promise ที่ fulfilled ให้อัตโนมัติ ต้องใช้ await หรือ .then() เพื่อดึงค่าจริงออกมาใช้งานเสมอ

เขียน async/await กับ setTimeout ได้เลยไหมโดยไม่ต้องห่อเป็น Promise ไม่ได้โดยตรง เพราะ setTimeout ไม่ได้คืน Promise มาให้เหมือน fetch ต้องห่อด้วยตัวเองผ่าน new Promise() ก่อนเสมอ อย่างที่แสดงในตัวอย่างฟังก์ชัน delay ด้านบน ถึงจะใช้ร่วมกับ await ได้อย่างถูกต้อง

สรุป

Promise คือ object ที่ใช้แทนผลลัพธ์ของงานที่ยังทำไม่เสร็จในตอนนี้ มี 3 สถานะคือ pending, fulfilled และ rejected

จุดสำคัญที่ต้องจำ

  • ใช้ .then()/.catch() หรือ async/await ในการจัดการผลลัพธ์ ทั้งสองทำงานเหมือนกัน ต่างแค่รูปแบบการเขียน
  • async/await อ่านง่ายกว่าเมื่อมีงานต่อเนื่องกันหลายขั้นตอน ใช้ try/catch จัดการ error ได้เหมือนโค้ดปกติ
  • ใช้ Promise.all() เมื่อต้องการรอหลายงานพร้อมกันแทนที่จะรอทีละตัว

เริ่มฝึกจากการใช้ fetch() ร่วมกับ async/await ให้คล่องก่อน จะเข้าใจภาพรวมของ Promise ได้ชัดเจนขึ้นมากจากการใช้งานจริง มากกว่าอ่านทฤษฎีเพียงอย่างเดียวโดยไม่ได้ลองเขียนโค้ดเอง

1 thought on “Promise ใน JavaScript คืออะไร (พร้อมตัวอย่าง async/await)”

Leave a Comment