เจาะลึก JavaScript Promises และการเขียนโค้ดแบบ Async ให้โปรแกรมลื่นไหล

10 นาที 3 views บันทึกเป็น PDF
เจาะลึก JavaScript Promises และการเขียนโค้ดแบบ Async ให้โปรแกรมลื่นไหล

มือใหม่หัดเขียน JavaScript ต้องรู้! ทำความเข้าใจเรื่อง Promises, การจัดการสถานะ และการใช้ async/await เพื่อดึงข้อมูลจาก API ได้แบบมือโปร ไม่ต้องกลัวโค้ดค้าง

ทำความรู้จักกับ JavaScript Promises และความสำคัญของการทำงานแบบไม่รอ

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

ลองนึกภาพว่าคุณไปสั่งกาแฟที่ร้านแล้วพนักงานให้ ใบเสร็จ (Receipt) มาแทนแก้วกาแฟทันที ใบเสร็จนั้นคือ Promise ครับ มันบอกคุณว่า "เดี๋ยวคุณจะได้กาแฟแน่นอน" ในระหว่างที่รอ คุณสามารถเดินไปทำธุระอื่นหรือนั่งเล่นมือถือได้โดยไม่ต้องยืนจ้องหน้าพนักงานตลอดเวลา เมื่อกาแฟเสร็จแล้ว พนักงานก็จะเรียกคุณไปรับของ นี่คือแนวคิดของ Asynchronous Programming (การเขียนโปรแกรมแบบไม่ต้องรอให้งานหนึ่งเสร็จก่อนถึงจะเริ่มงานถัดไป) ที่ช่วยให้โปรแกรมลื่นไหล

สำหรับมือใหม่ การเข้าใจ Promise ไม่ใช่แค่การจำคำสั่ง แต่คือการเปลี่ยนวิธีคิดจากการทำงาน "ทีละบรรทัด" มาเป็น "การเตรียมรับมือกับผลลัพธ์ที่จะตามมา" เมื่อคุณเข้าใจเรื่องนี้แล้ว คุณจะสามารถเขียนแอปพลิเคชันที่จัดการกับ API (ช่องทางเชื่อมต่อข้อมูลระหว่างโปรแกรม) หรือฐานข้อมูลได้อย่างมืออาชีพ และลดโอกาสที่จะเกิดบั๊ก (ข้อผิดพลาดของโปรแกรม) ที่หาสาเหตุยากไปได้มหาศาลครับ

สถานะของ Promise และการทำงานเบื้องต้น

Promise หนึ่งตัวจะมีสถานะ (State) ที่เปลี่ยนไปได้เพียงครั้งเดียวและไม่สามารถย้อนกลับได้ เริ่มต้นจากสถานะ Pending (กำลังรอผลลัพธ์) แล้วจะจบลงที่สถานะใดสถานะหนึ่ง คือ Fulfilled (ทำสำเร็จ) หรือ Rejected (ทำไม่สำเร็จ/เกิดข้อผิดพลาด) การเข้าใจสถานะเหล่านี้สำคัญมาก เพราะจะทำให้เรารู้ว่าตอนไหนควรทำอะไรต่อ

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

ลองมาดูตัวอย่างการสร้าง Promise ง่ายๆ กันครับ เราจะจำลองการรอข้อมูลด้วย setTimeout (คำสั่งหน่วงเวลา) ซึ่งเป็นพื้นฐานที่ใช้บ่อยในการทดสอบการทำงานแบบไม่รอของ JavaScript

const myPromise = new Promise((resolve, reject) => {
  const success = true; // ลองเปลี่ยนเป็น false ดูผลลัพธ์
  setTimeout(() => {
    if (success) {
      resolve("ทำงานสำเร็จแล้ว!"); // ส่งค่าออกไปเมื่อสำเร็จ
    } else {
      reject("เกิดข้อผิดพลาด!"); // ส่งค่าออกไปเมื่อล้มเหลว
    }
  }, 1000);
});

myPromise
  .then((result) => console.log(result)) // แสดงผลเมื่อสำเร็จ
  .catch((error) => console.error(error)); // แสดงผลเมื่อพลาด

อธิบายโค้ดทีละส่วน: new Promise คือการสร้างสัญญาขึ้นมา resolve คือฟังก์ชันที่เรียกเมื่อทุกอย่างโอเค ส่วน reject คือฟังก์ชันที่เรียกเมื่อเกิดปัญหา setTimeout จำลองการทำงาน 1 วินาที และ .then()/.catch() คือตัวดักจับผลลัพธ์ที่ส่งออกมาครับ

ผลลัพธ์ที่ควรเห็น: หลังจากผ่านไป 1 วินาที ถ้า success เป็น true หน้าจอจะพิมพ์คำว่า "ทำงานสำเร็จแล้ว!" แต่ถ้าเปลี่ยนเป็น false จะแสดงคำว่า "เกิดข้อผิดพลาด!" ออกมาทาง Console (หน้าต่างแสดงผลสำหรับนักพัฒนา) แทนครับ

การเขียน Async และ Await เพื่อโค้ดที่อ่านง่าย

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

การใช้ async/await ทำให้โค้ดของเราดูเหมือนการเขียนแบบปกติที่ทำทีละบรรทัด (Synchronous) ทำให้เราอ่านลำดับการทำงานได้ง่ายขึ้นมาก สิ่งที่สำคัญคือการครอบโค้ดด้วย try...catch (บล็อกสำหรับดักจับข้อผิดพลาด) เพื่อให้เราจัดการกับปัญหาที่อาจเกิดขึ้นได้อย่างปลอดภัยในทุกขั้นตอน

นี่คือตัวอย่างการนำ Promise มาใช้งานจริงในการดึงข้อมูลสมมติ ซึ่งเป็นสถานการณ์ที่คุณจะเจอแน่นอนเมื่อต้องทำงานกับ API หรือฐานข้อมูลในอนาคตครับ

async function fetchData() {
  try {
    console.log("เริ่มดึงข้อมูล...");
    const data = await new Promise((resolve) => 
      setTimeout(() => resolve("ข้อมูลได้รับแล้ว!"), 1000)
    );
    console.log(data);
  } catch (error) {
    console.error("มีปัญหาเกิดขึ้น:", error);
  } finally {
    console.log("จบการทำงาน");
  }
}

fetchData();

อธิบายโค้ดทีละส่วน: async นำหน้าฟังก์ชันเพื่อให้ใช้ await ได้ try คือส่วนที่รันโค้ดปกติ await จะหยุดรอจนกว่า Promise จะคืนค่า catch จะทำงานทันทีถ้ามีบรรทัดไหนใน try เกิด Error และ finally จะทำงานเสมอไม่ว่าจะสำเร็จหรือพังครับ

ผลลัพธ์ที่ควรเห็น: หน้าจอจะพิมพ์ "เริ่มดึงข้อมูล..." ทันที หลังจากนั้น 1 วินาทีจะพิมพ์ "ข้อมูลได้รับแล้ว!" และปิดท้ายด้วย "จบการทำงาน" เสมอ ไม่ว่าการดึงข้อมูลจะสำเร็จหรือไม่ก็ตาม

ข้อควรระวัง: มือใหม่มักลืมใส่ await หน้า Promise ทำให้โปรแกรมทำงานข้ามขั้นตอนไปก่อนที่ข้อมูลจะมาถึงเสมอ ให้เช็คให้ดีว่าเรากำลังรอค่าจาก Promise อยู่หรือไม่ครับ

การทำงานแบบขนานด้วย Promise Combinators

บางครั้งเรามีงานหลายอย่างที่ทำพร้อมกันได้โดยไม่ต้องรอให้เสร็จทีละตัว เช่น การดึงรูปภาพ 3 รูปจากเซิร์ฟเวอร์ ถ้าเราเขียน await ทีละตัว โปรแกรมจะช้ามาก เราจึงต้องใช้ Promise Combinators (ชุดคำสั่งจัดการ Promise หลายตัวพร้อมกัน) อย่างเช่น Promise.all()

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

ลองดูตัวอย่างการดึงข้อมูลหลายอย่างพร้อมกัน ซึ่งช่วยประหยัดเวลาการทำงานของแอปพลิเคชันไปได้มหาศาลครับ

async function runParallel() {
  const p1 = new Promise(resolve => setTimeout(() => resolve("งาน 1 เสร็จ"), 500));
  const p2 = new Promise(resolve => setTimeout(() => resolve("งาน 2 เสร็จ"), 1000));

  const results = await Promise.all([p1, p2]);
  console.log(results); // ["งาน 1 เสร็จ", "งาน 2 เสร็จ"]
}

runParallel();

อธิบายโค้ดทีละส่วน: เราสร้าง Promise สองตัวที่มีเวลาต่างกัน Promise.all จะรอจนกว่าตัวที่ช้าที่สุด (1000ms) จะเสร็จ แล้วค่อยคืนค่ากลับมาเป็น Array (รายการข้อมูล) ของผลลัพธ์ทั้งหมดครับ

ผลลัพธ์ที่ควรเห็น: หลังจากผ่านไป 1 วินาที หน้าจอจะแสดง Array ของผลลัพธ์ที่ได้จากทั้งสองงานพร้อมกัน นี่คือการเพิ่มประสิทธิภาพ (Performance) ที่สำคัญมากสำหรับโปรแกรมเมอร์ครับ

การจัดการข้อผิดพลาดและ Timeout

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

Promise.race() จะส่งผลลัพธ์ของตัวที่ทำงานเสร็จเร็วที่สุดกลับมา ไม่ว่าจะเป็นความสำเร็จหรือความล้มเหลว หากเราสร้าง Promise ที่จะพังหลังจาก 5 วินาทีมาแข่งกับ Promise ดึงข้อมูล ถ้าข้อมูลมาไม่ทัน 5 วินาที ตัวพังจะชนะและทำให้เราสามารถแจ้งเตือนผู้ใช้ได้ทันทีว่า "การเชื่อมต่อมีปัญหา"

การเขียนโปรแกรมที่ดีต้องครอบคลุมกรณีที่เลวร้ายที่สุดเสมอ การใส่ระบบ Timeout จะช่วยให้โปรแกรมของคุณดูเป็นมืออาชีพและลดความหงุดหงิดของผู้ใช้งานเมื่อระบบมีปัญหาครับ

const slowTask = new Promise(res => setTimeout(() => res("เสร็จ!"), 5000));
const timeout = new Promise((_, rej) => setTimeout(() => rej("หมดเวลา!"), 2000));

Promise.race([slowTask, timeout])
  .then(res => console.log(res))
  .catch(err => console.error(err));

อธิบายโค้ดทีละส่วน: slowTask คืองานที่ใช้เวลา 5 วินาที timeout คือการสร้างความล้มเหลวใน 2 วินาที Promise.race จะตัดสินว่าใครเสร็จก่อน ซึ่งในที่นี้คือตัว timeout ครับ

ผลลัพธ์ที่ควรเห็น: หลังจาก 2 วินาที คุณจะเห็นข้อความ "หมดเวลา!" แสดงขึ้นมาแทนที่จะรอจนครบ 5 วินาที นี่คือวิธีรับมือกับสถานการณ์ที่ระบบไม่ตอบสนองครับ

สรุป: การก้าวสู่การเป็นโปรแกรมเมอร์ที่เชี่ยวชาญ

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

ในฐานะพี่เลี้ยง ผมแนะนำว่าให้ลองสร้างโปรเจกต์เล็กๆ เช่น เว็บไซต์ดึงข้อมูลสภาพอากาศหรือรายการสินค้าจาก API ฟรีดูครับ ลองใช้ async/await ในการดึงข้อมูล และใช้ try...catch ในการจัดการกรณีที่เน็ตหลุดหรือข้อมูลผิดพลาด การลงมือทำจริงจะช่วยให้คุณเห็นภาพว่า Promise ทำงานอย่างไรในสถานการณ์ที่คาดเดาไม่ได้

หัวใจสำคัญคือการฝึกฝน อย่ากลัวที่จะเจอ Error เพราะทุกครั้งที่ Promise พัง นั่นคือโอกาสที่คุณจะได้เรียนรู้วิธีการแก้ปัญหาและพัฒนาทักษะการเขียนโค้ดให้แกร่งขึ้น ขอให้สนุกกับการเขียน JavaScript และก้าวไปสู่การเป็นโปรแกรมเมอร์ที่เก่งขึ้นในทุกๆ วันครับ


ที่มา: Deep Dive into JavaScript Promises: Async Patterns and Error Handling — DEV Community

แชร์บทความ

Facebook X LINE

บทความที่เกี่ยวข้อง

ป้องกันข้อมูลรั่วไหลในระบบ Multi-Tenancy ด้วย PostgreSQL Row-Level Security

ป้องกันข้อมูลรั่วไหลในระบบ Multi-Tenancy ด้วย PostgreSQL Row-Level Security

เบื่อไหมกับการต้องคอยเขียน WHERE tenant_id ทุกครั้ง? มาเรียนรู้วิธีใช้ Row-Level Security (RLS) ใน PostgreSQL เพื่อแยกข้อมูลลูกค้าให้ปลอดภัยแบบอัตโนมัติ

ที่มา: DEV Community

8 hours ago 10 นาที
5 views
วิธีทำระบบล็อกอิน OIDC ให้แอป Jakarta EE ด้วย pac4j

วิธีทำระบบล็อกอิน OIDC ให้แอป Jakarta EE ด้วย pac4j

อยากทำระบบล็อกอินด้วย Google หรือ Microsoft ใน Jakarta EE ใช่ไหม? มาดูวิธีใช้ pac4j จัดการความปลอดภัยแบบมือโปร ไม่ต้องเขียนเองตั้งแต่ต้นให้ปวดหัว

ที่มา: DEV Community

12 hours ago 11 นาที
6 views
เข้าใจ Prototype ใน JavaScript: คู่มือฉบับเข้าใจง่ายสำหรับมือใหม่

เข้าใจ Prototype ใน JavaScript: คู่มือฉบับเข้าใจง่ายสำหรับมือใหม่

อยากเขียน JavaScript ให้เก่งขึ้นต้องรู้เรื่อง Prototype! มาทำความเข้าใจพิมพ์เขียวกลางที่ช่วยให้โค้ดสะอาด ประหยัดหน่วยความจำ และสืบทอดคุณสมบัติได้แบบมือโปร

ที่มา: DEV Community

14 hours ago 9 นาที
4 views