จากนรกของ Callback สู่แสงสว่างที่เรียกว่า Promise: ประสบการณ์ตรงของคนเขียนโค้ด
ย้อนกลับไปเมื่อประมาณ 7-8 ปีก่อน สมัยที่ผมเริ่มหัดเขียน JavaScript ใหม่ๆ สิ่งหนึ่งที่ทำให้ผมแทบอยากจะวางมือจากการเขียนโปรแกรมคือการจัดการกับ “ลำดับการทำงาน” ของโค้ดที่ต้องรอผลลัพธ์จากเซิร์ฟเวอร์ ในยุคนั้นเรายังไม่มีเครื่องมือที่หรูหราอย่างปัจจุบัน เราอยู่กับสิ่งที่เรียกว่า Callback Function ซึ่งถ้ามองเผินๆ มันก็ดูไม่มีพิษมีภัยอะไร แต่วันหนึ่งเมื่อโปรเจกต์เริ่มใหญ่ขึ้น ผมก็พบว่าตัวเองกำลังจมอยู่ในกองโค้ดที่ซ้อนกันจนหาจุดจบไม่เจอ
ปัญหาที่ผมเจอคือ “Callback Hell” หรือที่เราเรียกกันเล่นๆ ว่าพีระมิดแห่งความตาย (Pyramid of Doom) มันคือสถานการณ์ที่ฟังก์ชัน A ต้องรอ B, B ต้องรอ C, และ C ต้องรอ D โค้ดจะเยื้องขวาไปเรื่อยๆ จนอ่านไม่รู้เรื่อง แถมการดักจับ Error ก็เป็นฝันร้าย เพราะเราต้องเขียน `if (err)` ในทุกชั้นของฟังก์ชัน จนกระทั่งผมได้รู้จักกับสิ่งที่เรียกว่า Promise มันไม่ได้เป็นแค่ฟีเจอร์ใหม่ แต่มันคือการเปลี่ยนวิธีคิด (Mindset) ในการเขียนโปรแกรมแบบ Asynchronous ไปอย่างสิ้นเชิง
1. เมื่อ Callback กลายเป็นกับดักที่ทำให้ผมเกือบถอดใจ

Photo by Digital Buggu on Pexels
ในโลกของการเขียนเว็บ การทำงานแบบ Asynchronous เป็นเรื่องที่เลี่ยงไม่ได้เลยครับ ไม่ว่าจะเป็นการดึงข้อมูลจาก API, การอ่านไฟล์ หรือการตั้งเวลา (SetTimeout) ในช่วงแรกผมเขียนโค้ดโดยใช้ Callback เพราะมันเป็นวิธีมาตรฐานเดียวที่มีในตอนนั้น ผมจำได้แม่นว่าตอนที่ต้องทำระบบสั่งซื้อสินค้า ซึ่งต้องเริ่มจากเช็คสต็อก, ตัดเงิน, และส่งอีเมลยืนยัน โค้ดของผมมันกลายเป็นรูปตัว V ที่ซ้อนกัน 5-6 ชั้น
ความซับซ้อนนี้ไม่ได้ทำให้อ่านยากอย่างเดียว แต่มันทำให้การ Debug เป็นเรื่องที่เป็นไปไม่ได้เลย หากขั้นตอนที่ 3 เกิด Error ผมแทบจะระบุไม่ได้ว่ามันพังมาจากจุดไหน หรือจะจัดการส่ง Error กลับไปหา User อย่างไรให้ดูดีที่สุด ประสบการณ์นั้นสอนให้ผมรู้ว่า การเขียนโค้ดให้ “ทำงานได้” กับการเขียนโค้ดให้ “ดูแลรักษาได้” มันต่างกันลิบลับ และนั่นคือตอนที่ผมเริ่มมองหาทางออกที่ดีกว่า
ทำไม Callback ถึงไม่ตอบโจทย์ในระยะยาว?
ปัญหาหลักของ Callback คือการสูญเสียการควบคุม (Inversion of Control) เราส่งฟังก์ชันของเราไปให้ฟังก์ชันอื่นเรียกใช้ ซึ่งเราไม่รู้เลยว่ามันจะถูกเรียกกี่ครั้ง หรือจะถูกเรียกด้วย Parameter ที่ถูกต้องไหม นอกจากนี้เรื่องของ Error Handling ยังกระจัดกระจาย ทำให้โค้ดของเราเต็มไปด้วย Logic ของการเช็ค Error ซ้ำซ้อน จนบดบังหัวใจสำคัญของโปรแกรมที่เรากำลังเขียนอยู่
2. Promise คืออะไร? คำสัญญาที่เปลี่ยนโลกการเขียนโค้ด
ถ้าจะให้อธิบายให้เห็นภาพที่สุด Promise ก็เหมือนกับการที่เราไปสั่งอาหารที่ร้าน Fast Food ครับ เมื่อเราจ่ายเงินเสร็จ พนักงานจะไม่ให้เบอร์เกอร์เราทันที แต่จะให้ “เครื่องเรียกคิว” (Pager) มาแทน เครื่องนี้แหละคือ Promise มันเป็นตัวแทนของผลลัพธ์ที่เราคาดหวังในอนาคต ซึ่งในระหว่างที่รอ เราจะไปนั่งเล่นมือถือ หรือคุยกับเพื่อนก็ได้ โดยไม่ต้องยืนจ้องหน้าพนักงานตลอดเวลา
ในทางเทคนิค Promise คือ Object ที่แทนค่าที่อาจจะเสร็จสมบูรณ์ (Resolved) หรือล้มเหลว (Rejected) ในอนาคต มันมีสถานะสำคัญ 3 อย่างคือ Pending (กำลังรอ), Fulfilled (สำเร็จ), และ Rejected (ล้มเหลว) การมีสถานะที่ชัดเจนแบบนี้ทำให้เราสามารถจัดการกับผลลัพธ์ได้อย่างเป็นระบบ ไม่ว่ามันจะออกมาในรูปแบบไหนก็ตาม
สถานะทั้ง 3 ของ Promise ที่คุณต้องเข้าใจ
1. Pending: สถานะเริ่มต้น เมื่อเราเริ่มสั่งงานแต่ยังไม่ได้ผลลัพธ์ 2. Fulfilled: เมื่อการทำงานสำเร็จและได้ข้อมูลตามที่ต้องการ 3. Rejected: เมื่อเกิดข้อผิดพลาดบางอย่างขึ้น เช่น Server ล่ม หรือหาไฟล์ไม่เจอ การเข้าใจสถานะเหล่านี้ช่วยให้ผมวางแผนการเขียนโปรแกรมได้รัดกุมขึ้นมากครับ
// ตัวอย่างการสร้างและใช้งาน Promise เบื้องต้น
const checkStock = (productName) => {
return new Promise((resolve, reject) => {
console.log(`กำลังตรวจสอบสต็อกสินค้า: ${productName}...`);
setTimeout(() => {
const isAvailable = true; // สมมติว่าเช็คจาก Database
if (isAvailable) {
resolve("สินค้ามีพร้อมส่ง!");
} else {
reject("ขออภัย สินค้าหมดสต็อก");
}
}, 2000);
});
};
checkStock("iPhone 15")
.then((result) => console.log(result))
.catch((error) => console.error(error));
3. การแก้ปัญหาด้วย Chaining: จากซ้อนกันเป็นเส้นตรง
ฟีเจอร์ที่ผมประทับใจที่สุดของ Promise คือการทำ “Chaining” หรือการร้อยเรียงฟังก์ชันเข้าด้วยกันผ่านเมธอด `.then()` แทนที่เราจะเขียนโค้ดซ้อนเข้าไปข้างใน เราเขียนมันต่อท้ายกันลงมาเป็นลำดับขั้น ทำให้โค้ดอ่านจากบนลงล่างเหมือนอ่านหนังสือทั่วไป ซึ่งมันช่วยลดภาระทางสมอง (Cognitive Load) ของนักพัฒนาไปได้มหาศาล
จากเดิมที่ผมต้องเขียน Callback ซ้อนกัน 5 ชั้น ผมเปลี่ยนมาใช้ `.then()` ต่อกันไปเรื่อยๆ ข้อมูลจากขั้นตอนที่ 1 จะถูกส่งต่อไปยังขั้นตอนที่ 2 อย่างเป็นระบบ และสิ่งที่เจ๋งที่สุดคือเราสามารถใช้ `.catch()` เพียงตัวเดียวที่ท้ายสุด เพื่อดักจับ Error ที่อาจเกิดขึ้นในขั้นตอนใดก็ได้ใน Chain นั้น วิธีนี้ทำให้โค้ดสะอาดขึ้นและจัดการ Error ได้รวมศูนย์ (Centralized Error Handling) อย่างที่ไม่เคยทำได้มาก่อน
หัวใจของการทำ Promise Chaining
ความลับของการ Chaining คือทุกครั้งที่เรา Return ค่าจากภายใน `.then()` มันจะถูก Wrap กลับมาเป็น Promise เสมอ ทำให้เราสามารถเรียก `.then()` ต่อไปได้เรื่อยๆ ไม่รู้จบ ประสบการณ์นี้ทำให้ผมเลิกกลัวการเขียน Logic ซับซ้อน เพราะผมรู้แล้วว่าผมสามารถย่อยมันออกมาเป็นขั้นตอนย่อยๆ ที่เชื่อมต่อกันได้อย่างสวยงาม
4. ตัวอย่างการใช้งานจริง: การดึงข้อมูลจาก API
เพื่อให้เห็นภาพชัดเจนขึ้น ลองมาดูสถานการณ์ที่ผมต้องดึงข้อมูล User จาก API แล้วต้องเอา ID ของ User นั้นไปดึงรายการสั่งซื้อต่อ ถ้าเป็นสมัยก่อนคงวุ่นวายพิลึก แต่ด้วย Promise และฟังก์ชัน `fetch` ของ JavaScript สมัยใหม่ ทุกอย่างดูง่ายไปหมด โค้ดด้านล่างนี้คือรูปแบบที่ผมใช้จริงในงานปัจจุบัน ซึ่งมันช่วยลดข้อผิดพลาดในการทำงานได้ดีมาก
การใช้ Promise ในลักษณะนี้ยังช่วยให้เราสามารถทำสิ่งที่เรียกว่า “Parallel Execution” ได้ด้วย เช่น ถ้าเราต้องการดึงข้อมูลจาก 3 แหล่งพร้อมกันโดยไม่ต้องรอกัน เราสามารถใช้ `Promise.all()` เพื่อเริ่มทำงานพร้อมกันและรอผลลัพธ์ทั้งหมดทีเดียว ซึ่งช่วยเพิ่ม Performance ของเว็บแอปพลิเคชันได้อย่างน่าทึ่งเมื่อเทียบกับวิธี Callback เดิมๆ
// การดึงข้อมูล API ซ้อนกันด้วย Promise Chaining
fetch('https://api.example.com/user/1')
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(user => {
console.log(`สวัสดีคุณ: ${user.name}`);
return fetch(`https://api.example.com/orders/${user.id}`);
})
.then(response => response.json())
.then(orders => {
console.log(`คุณมีรายการสั่งซื้อทั้งหมด: ${orders.length} รายการ`);
})
.catch(error => {
console.error('เกิดข้อผิดพลาดในขั้นตอนใดขั้นตอนหนึ่ง:', error);
});
5. บทสรุปของความเปลี่ยนแปลง: ทำไมคุณถึงต้องใช้ Promise
หลังจากที่ผมเปลี่ยนมาใช้ Promise ในทุกโปรเจกต์ สิ่งที่เห็นได้ชัดคือเวลาที่ใช้ในการ Debug ลดลงอย่างมาก โค้ดของทีมอ่านง่ายขึ้น ทุกคนเข้าใจตรงกันว่าจุดไหนคือจุดเริ่มต้นและจุดจบของกระบวนการ และที่สำคัญที่สุดคือมันเป็นพื้นฐานสำคัญไปสู่ `async/await` ซึ่งเป็น Syntax ที่สวยงามยิ่งกว่าใน JavaScript ปัจจุบัน
สำหรับน้องๆ นักพัฒนาที่เพิ่งเริ่มต้น ผมแนะนำว่าอย่าเพิ่งข้ามผ่าน Promise ไปใช้ `async/await` ทันทีโดยไม่เข้าใจพื้นฐาน เพราะท้ายที่สุดแล้ว `async/await` ก็คือการเขียน Promise ในรูปแบบที่ดูเหมือน Synchronous เท่านั้น การเข้าใจสถานะและการจัดการ Error ของ Promise จะทำให้คุณเป็นนักพัฒนาที่เขียนโค้ดได้อย่างแข็งแรงและรับมือกับปัญหาที่ซับซ้อนได้จริง
สรุปประเด็นสำคัญของ Promise
- ความอ่านง่าย (Readability): เปลี่ยนจากโค้ดที่ซ้อนกันจนตาลาย ให้กลายเป็นลำดับขั้นตอนที่ชัดเจนจากบนลงล่าง
- การจัดการ Error (Error Handling): ใช้ .catch() เพียงจุดเดียวเพื่อดักจับปัญหาในทุกขั้นตอนของกระบวนการ
- ความสามารถในการขยาย (Scalability): สามารถเชื่อมต่อ (Chain) หรือทำงานขนาน (Parallel) ได้อย่างอิสระและเป็นระบบ
- มาตรฐานสากล: เป็นมาตรฐานของ JavaScript สมัยใหม่ที่รองรับโดย Browser และ Node.js ทุกเวอร์ชันในปัจจุบัน
สรุป
การเรียนรู้เรื่อง Promise สำหรับผมไม่ใช่แค่การเรียนรู้คำสั่งใหม่ แต่มันคือการเรียนรู้วิธีการจัดการกับ “ความไม่แน่นอน” ในโลกของการเขียนโปรแกรม เราไม่รู้ว่า API จะตอบกลับมาเมื่อไหร่ หรือเน็ตจะหลุดตอนไหน แต่ด้วย Promise เรามีเครื่องมือที่บอกว่า “ไม่ว่าอะไรจะเกิดขึ้น เรามีแผนรับมือไว้แล้ว” และนั่นคือหัวใจสำคัญที่ทำให้เราสร้างแอปพลิเคชันที่ลื่นไหลและน่าเชื่อถือให้กับผู้ใช้งานได้ในที่สุดครับ





