Article · programming
JavaScript Error Handling และ try...catch
JavaScript
เรียนรู้การจัดการข้อผิดพลาดใน JavaScript ด้วย try...catch, finally, throw, Error objects และการรับมือกับข้อผิดพลาดใน Async/Await เพื่อสร้างโปรแกรมที่ตรวจสอบปัญหาได้และทำงานได้อย่างน่าเชื่อถือ
EP15: จัดการข้อผิดพลาดด้วย JavaScript Error Handling
โปรแกรมที่ดีไม่ได้หมายถึงโปรแกรมที่ไม่มีข้อผิดพลาดเลย แต่หมายถึงโปรแกรมที่สามารถตรวจพบปัญหา จัดการสถานการณ์ที่ไม่คาดคิด และแจ้งผลให้ผู้ใช้เข้าใจได้อย่างเหมาะสม JavaScript มีเครื่องมือสำหรับจัดการข้อผิดพลาด เช่น `try...catch`, `finally`, `throw` และออบเจ็กต์ `Error` ซึ่งช่วยให้เราควบคุมการทำงานเมื่อเกิดปัญหาได้ บทนี้จะเริ่มจากข้อผิดพลาดพื้นฐาน ไปจนถึงการจัดการข้อผิดพลาดในฟังก์ชันแบบ Async/Await
เป้าหมายของบทเรียนเมื่อเรียนจบ คุณจะสามารถแยกประเภทข้อผิดพลาด ใช้ try...catch เพื่อดักจับปัญหา ใช้ finally สำหรับงานที่ต้องทำท้ายสุด สร้างข้อผิดพลาดด้วย throw ตรวจสอบข้อมูลก่อนประมวลผล และจัดการข้อผิดพลาดจาก Promise และ Async/Await ได้
1. ข้อผิดพลาดใน JavaScript มีอะไรบ้าง
ข้อผิดพลาดในโปรแกรมเกิดขึ้นได้หลายรูปแบบ บางปัญหาทำให้โค้ดไม่สามารถเริ่มทำงานได้ บางปัญหาเกิดขึ้นระหว่างการทำงาน และบางปัญหาไม่ทำให้โปรแกรมหยุดแต่ทำให้ผลลัพธ์ผิดจากที่ต้องการ การแยกประเภทช่วยให้เราเลือกวิธีแก้ปัญหาได้ตรงจุด
| ประเภท | ความหมาย | ตัวอย่าง |
|---|---|---|
| Syntax Error | ไวยากรณ์ไม่ถูกต้อง | วงเล็บหรือเครื่องหมายคำพูดไม่ครบ |
| Runtime Error | เกิดข้อผิดพลาดขณะโปรแกรมทำงาน | เรียก Property จาก null |
| Logical Error | โค้ดทำงานได้ แต่ผลลัพธ์ผิด | ใช้เครื่องหมายคำนวณผิด |
| Validation Error | ข้อมูลไม่ผ่านกฎที่กำหนด | อายุเป็นค่าติดลบ |
Syntax Error มักต้องแก้ไขรูปแบบโค้ดก่อนจึงจะทำงานได้ ส่วน Runtime Error หลายกรณีสามารถจัดการด้วย try...catch ได้ สำหรับ Logical Error โปรแกรมอาจทำงานจนจบโดยไม่โยนข้อผิดพลาด เราจึงต้องใช้การทดสอบและตรวจสอบเงื่อนไขเพิ่มเติม
const price = 100;
const quantity = 3;
const correctTotal = price * quantity;
const incorrectTotal = price + quantity;
console.log(correctTotal);
console.log(incorrectTotal);
ตัวอย่างนี้ไม่มี Syntax Error หรือ Runtime Error แต่ `incorrectTotal` ให้ค่า 103 แทนที่จะเป็น 300 เพราะเลือกใช้การบวกแทนการคูณ นี่คือ Logical Error ซึ่งต้องแก้จากความถูกต้องของตรรกะ ไม่ใช่เพียงดักจับข้อผิดพลาด
2. ใช้ try...catch เพื่อดักจับข้อผิดพลาด
`try...catch` ใช้ครอบโค้ดที่อาจเกิดข้อผิดพลาด หากมีข้อผิดพลาดที่ถูกโยนขึ้นมาระหว่างการทำงานในบล็อก `try` การทำงานจะข้ามไปยังบล็อก `catch` แทนการหยุดไหลของโค้ด ณ จุดนั้น วิธีนี้ช่วยให้โปรแกรมตอบสนองต่อปัญหาได้อย่างควบคุมได้
try {
const user = null;
console.log(user.name);
console.log('บรรทัดนี้จะไม่ทำงาน');
} catch (error) {
console.log('เกิดข้อผิดพลาด:', error.message);
}
console.log('โปรแกรมยังทำงานต่อได้');
ในตัวอย่าง การอ่าน `user.name` ทำให้เกิด TypeError เพราะ `user` เป็น `null` เมื่อข้อผิดพลาดเกิดขึ้น JavaScript จะข้ามบรรทัดที่เหลือในบล็อก `try` แล้วเข้าสู่ `catch` จากนั้นจึงทำงานต่อจากคำสั่งหลังโครงสร้าง try...catch
หลักการสำคัญวางเฉพาะโค้ดที่มีโอกาสเกิดข้อผิดพลาดไว้ใน try ให้มากพอสำหรับจัดการปัญหา แต่ไม่ควรครอบโค้ดทั้งโปรแกรมโดยไม่จำเป็น เพราะจะทำให้ค้นหาสาเหตุและระบุจุดที่ล้มเหลวได้ยาก
อ่านข้อมูลจาก Error Object
ค่าที่รับเข้ามาใน `catch` มักเป็น Error Object ซึ่งมี Property ที่สำคัญ เช่น `name` และ `message` ส่วน `stack` มักมีรายละเอียดลำดับการเรียกฟังก์ชันที่ช่วยวิเคราะห์ปัญหา โดยรูปแบบและรายละเอียดของ Stack อาจแตกต่างกันตาม JavaScript Runtime
try {
throw new Error('ไม่สามารถบันทึกข้อมูลได้');
} catch (error) {
console.log('ประเภท:', error.name);
console.log('ข้อความ:', error.message);
console.log('รายละเอียด:', error.stack);
}
`error.name` ช่วยบอกประเภทของข้อผิดพลาด `error.message` อธิบายปัญหาในรูปแบบข้อความ และ `error.stack` มีประโยชน์สำหรับการตรวจสอบระหว่างพัฒนา แต่ไม่ควรนำรายละเอียดภายในระบบหรือ Stack Trace ไปแสดงต่อผู้ใช้ทั่วไปโดยไม่ผ่านการพิจารณา
3. finally สำหรับงานที่ต้องทำท้ายสุด
บล็อก `finally` ใช้เขียนคำสั่งที่ต้องการให้ทำหลังจากการทำงานใน try และ catch ไม่ว่าจะเกิดข้อผิดพลาดหรือไม่ ตัวอย่างการใช้งาน ได้แก่ การคืนสถานะการโหลด การปลดล็อกทรัพยากร หรือการทำความสะอาดสถานะชั่วคราว
function loadData() {
console.log('เริ่มโหลดข้อมูล');
try {
throw new Error('แหล่งข้อมูลไม่พร้อมใช้งาน');
} catch (error) {
console.log('โหลดไม่สำเร็จ:', error.message);
} finally {
console.log('สิ้นสุดกระบวนการโหลด');
}
}
loadData();
ในตัวอย่าง `finally` จะทำงานหลังจาก `catch` แม้ในบล็อก `try` จะเกิดข้อผิดพลาดก็ตาม โดยทั่วไปควรใช้ finally สำหรับการเก็บกวาดหรือคืนสถานะ ไม่ควรใช้ `return` ใน finally โดยไม่จำเป็น เพราะอาจกลบค่าที่ return จาก try หรือ catch และทำให้การโยนข้อผิดพลาดเปลี่ยนไป
ข้อควรระวังfinally ไม่ได้หมายความว่าโค้ดจะทำงานได้สำเร็จเสมอไป หากกระบวนการทำงานถูกยุติจากภายนอกหรือ Runtime ปิดลง ก็อาจไม่สามารถรับประกันการทำงานได้ทุกกรณี ใช้สำหรับการจัดการตามปกติภายในโฟลว์ของโปรแกรม
4. สร้างข้อผิดพลาดด้วย throw
คำสั่ง `throw` ใช้โยนข้อผิดพลาดด้วยตัวเอง เหมาะกับกรณีที่โปรแกรมตรวจพบข้อมูลไม่ถูกต้องหรือสถานการณ์ที่ไม่สามารถดำเนินการต่อได้ ควรสร้าง Error Object ด้วย `new Error()` แทนการโยนข้อความธรรมดา เพื่อให้มีชื่อและรายละเอียดของข้อผิดพลาดอย่างเป็นมาตรฐาน
function calculateDiscount(price, percent) {
if (typeof price !== 'number' || !Number.isFinite(price)) {
throw new TypeError('ราคาต้องเป็นตัวเลขที่ถูกต้อง');
}
if (price < 0) {
throw new RangeError('ราคาต้องไม่ติดลบ');
}
if (typeof percent !== 'number' || percent < 0 || percent > 100) {
throw new RangeError('ส่วนลดต้องอยู่ระหว่าง 0 ถึง 100');
}
return price * (1 - percent / 100);
}
try {
console.log(calculateDiscount(1000, 15));
console.log(calculateDiscount(-50, 15));
} catch (error) {
console.log('คำนวณไม่ได้:', error.message);
}
ฟังก์ชันนี้ตรวจสอบชนิดและช่วงของข้อมูลก่อนคำนวณ `TypeError` เหมาะกับกรณีที่ชนิดข้อมูลไม่ตรงตามที่คาดหวัง ส่วน `RangeError` เหมาะกับค่าที่อยู่นอกช่วงที่อนุญาต เมื่อข้อมูลไม่ผ่านเงื่อนไข ฟังก์ชันจะโยนข้อผิดพลาดและไม่ทำงานต่อในส่วนที่เหลือของฟังก์ชัน
ข้อผิดพลาดที่สร้างขึ้นเอง
บางระบบต้องแยกประเภทปัญหาทางธุรกิจให้ชัดเจน เช่น สินค้าไม่มีในคลัง บัญชีไม่มีสิทธิ์ หรือคำสั่งซื้อไม่สามารถเปลี่ยนสถานะได้ เราสามารถสร้าง Class ที่สืบทอดจาก Error เพื่อกำหนดชื่อและข้อมูลเพิ่มเติมให้เหมาะกับระบบได้
class InsufficientStockError extends Error {
constructor(productId, requested) {
super(`สินค้า ${productId} มีจำนวนไม่เพียงพอ`);
this.name = 'InsufficientStockError';
this.productId = productId;
this.requested = requested;
}
}
function reserveStock(product, quantity) {
if (quantity > product.stock) {
throw new InsufficientStockError(product.id, quantity);
}
return product.stock - quantity;
}
try {
console.log(reserveStock({ id: 'P-10', stock: 3 }, 5));
} catch (error) {
if (error instanceof InsufficientStockError) {
console.log(error.name, error.productId, error.requested);
} else {
throw error;
}
}
การสร้าง Error Class ช่วยให้โค้ดเรียกใช้งานแยกข้อผิดพลาดแต่ละชนิดได้ด้วย `instanceof` และยังคงมี `message` กับ `stack` จาก Error พื้นฐาน หากข้อผิดพลาดไม่ใช่ชนิดที่คาดไว้ ควรส่งต่อให้ชั้นที่เหมาะสมจัดการแทนการกลืนข้อผิดพลาดทั้งหมด
5. ตรวจสอบข้อมูลก่อนเกิดข้อผิดพลาด
ไม่จำเป็นต้องรอให้เกิดข้อผิดพลาดแล้วจึงแก้ไขเสมอไป การตรวจสอบข้อมูลก่อนประมวลผลช่วยให้ควบคุมพฤติกรรมได้ดีขึ้น โดยเฉพาะข้อมูลที่รับจากฟอร์ม API ไฟล์ หรือฐานข้อมูล ควรตรวจสอบทั้งชนิดข้อมูล ค่าที่อนุญาต และเงื่อนไขทางธุรกิจ
function createUser(input) {
if (!input || typeof input !== 'object' || Array.isArray(input)) {
throw new TypeError('ข้อมูลผู้ใช้ต้องเป็น Object');
}
if (typeof input.name !== 'string' || input.name.trim() === '') {
throw new Error('กรุณาระบุชื่อผู้ใช้');
}
if (!Number.isInteger(input.age) || input.age < 0) {
throw new RangeError('อายุต้องเป็นจำนวนเต็มตั้งแต่ศูนย์ขึ้นไป');
}
return {
name: input.name.trim(),
age: input.age
};
}
try {
console.log(createUser({ name: ' Mali ', age: 25 }));
console.log(createUser({ name: '', age: -1 }));
} catch (error) {
console.log('ไม่สามารถสร้างผู้ใช้:', error.message);
}
การตรวจสอบนี้ป้องกันข้อมูลบางประเภทที่ไม่ตรงตามสัญญาของฟังก์ชัน และคืน Object ที่ผ่านการจัดรูปแบบแล้ว อย่างไรก็ตาม การตรวจสอบฝั่ง JavaScript ไม่สามารถแทนการตรวจสอบบนเซิร์ฟเวอร์ได้ หากข้อมูลมีผลต่อสิทธิ์ การชำระเงิน หรือความปลอดภัย ต้องตรวจสอบซ้ำในฝั่งที่เชื่อถือได้
Validation ไม่ใช่แค่การดักจับ ErrorValidation เป็นการตรวจสอบว่าข้อมูลตรงตามกฎหรือไม่ ส่วน try...catch ใช้รับมือกับข้อผิดพลาดที่ถูกโยนขึ้นมา ทั้งสองแนวทางทำงานร่วมกันได้ แต่มีหน้าที่แตกต่างกัน
6. ส่งต่อข้อผิดพลาดด้วย throw
บางครั้งฟังก์ชันระดับล่างรู้ว่ามีปัญหา แต่ไม่มีข้อมูลเพียงพอที่จะตัดสินใจว่าควรแสดงข้อความแบบใดให้ผู้ใช้ ในกรณีนี้ ฟังก์ชันสามารถบันทึกข้อมูลที่เหมาะสมแล้วโยนข้อผิดพลาดต่อไปยังชั้นที่รับผิดชอบได้ การใช้ `throw error` จะส่งต่อ Error เดิมโดยรักษาข้อมูลเดิมไว้
function parseQuantity(value) {
try {
const quantity = Number(value);
if (!Number.isInteger(quantity) || quantity < 1) {
throw new RangeError('จำนวนสินค้าต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป');
}
return quantity;
} catch (error) {
console.log('ตรวจพบปัญหาในการอ่านจำนวนสินค้า');
throw error;
}
}
try {
const quantity = parseQuantity('0');
console.log('จำนวนที่เลือก:', quantity);
} catch (error) {
console.log('ไม่สามารถดำเนินการ:', error.message);
}
ในตัวอย่าง ฟังก์ชัน `parseQuantity()` ตรวจพบข้อผิดพลาดและโยนต่อ ส่วนโค้ดระดับบนเลือกวิธีตอบสนองต่อปัญหาได้เอง การส่งต่อแบบนี้ช่วยให้แต่ละส่วนรับผิดชอบตามหน้าที่ แต่ไม่ควรจับข้อผิดพลาดแล้วโยนต่อโดยไม่มีประโยชน์หรือเพิ่มข้อมูลที่ซ้ำซ้อนโดยไม่จำเป็น
7. ข้อผิดพลาดจาก Async/Await
การเรียก API การอ่านไฟล์ หรือการทำงานที่ใช้เวลามักเกี่ยวข้องกับ Promise เมื่อ Promise ถูก Reject การใช้ `await` จะทำให้ข้อผิดพลาดถูกโยนขึ้นมาในฟังก์ชัน async ณ จุดที่รอผลลัพธ์ เราจึงใช้ try...catch รอบคำสั่ง await เพื่อจัดการความล้มเหลวได้
function getUserFromService(userId) {
return new Promise((resolve, reject) => {
if (userId <= 0) {
reject(new RangeError('รหัสผู้ใช้ไม่ถูกต้อง'));
return;
}
resolve({ id: userId, name: 'Mali' });
});
}
async function showUser(userId) {
try {
const user = await getUserFromService(userId);
console.log('ผู้ใช้:', user.name);
} catch (error) {
console.log('ไม่สามารถโหลดผู้ใช้:', error.message);
}
}
showUser(1);
showUser(0);
ฟังก์ชัน `showUser()` ใช้ await รอผลลัพธ์จาก Promise หาก Promise สำเร็จจะทำงานต่อใน try หากถูก Reject จะเข้าสู่ catch ทั้งนี้ฟังก์ชัน async จะคืน Promise เสมอ ดังนั้นโค้ดที่เรียก `showUser()` ก็สามารถรอหรือจัดการ Promise ที่ฟังก์ชันคืนกลับมาได้อีกชั้นหนึ่ง
ดักจับข้อผิดพลาดจาก Promise โดยตรง
function loadSettings() {
return Promise.reject(new Error('ไม่สามารถโหลดการตั้งค่าได้'));
}
loadSettings()
.then(settings => {
console.log(settings);
})
.catch(error => {
console.log('เกิดปัญหา:', error.message);
})
.finally(() => {
console.log('จบกระบวนการโหลดการตั้งค่า');
});
หากใช้ Promise Chain สามารถใช้ `.catch()` เพื่อรับมือกับการ Reject ที่ส่งต่อมาตามสาย `.then()` และใช้ `.finally()` สำหรับงานปิดท้ายได้ ต้องระวังว่า หากโค้ดภายใน `.catch()` โยนข้อผิดพลาดใหม่หรือคืน Promise ที่ถูก Reject ข้อผิดพลาดนั้นจะส่งต่อไปยังขั้นถัดไป
ข้อผิดพลาดที่มักถูกมองข้ามการเรียกฟังก์ชัน async โดยไม่ await และไม่จัดการ Promise ที่คืนกลับมา อาจทำให้เกิด Unhandled Rejection ได้ ต้องพิจารณาว่าควรรอผลลัพธ์ ใช้ try...catch หรือผูก .catch() ตามรูปแบบการทำงานของระบบ
8. ข้อผิดพลาดใน async ไม่ได้ถูกจับทุกกรณีโดยอัตโนมัติ
try...catch จะจับข้อผิดพลาดที่เกิดขึ้นแบบซิงโครนัสภายในบล็อก try และข้อผิดพลาดจาก Promise ที่ถูก await ภายในบล็อกนั้น แต่จะไม่จับการ Reject ของ Promise ที่ถูกเริ่มทำงานแล้วปล่อยให้ทำงานต่อโดยไม่ได้ await หรือไม่ได้เชื่อมต่อการจัดการข้อผิดพลาดไว้
function failLater() {
return Promise.reject(new Error('งานล้มเหลว'));
}
async function safeTask() {
try {
await failLater();
} catch (error) {
console.log('จับข้อผิดพลาดได้:', error.message);
}
}
async function anotherSafeTask() {
try {
failLater().catch(error => {
console.log('จัดการผ่าน catch:', error.message);
});
} catch (error) {
console.log('บล็อกนี้ไม่จับ Promise rejection ที่ไม่ได้ await');
}
}
safeTask();
anotherSafeTask();
ฟังก์ชัน `safeTask()` ใช้ await ทำให้การ Reject กลายเป็นข้อผิดพลาดที่ try...catch จับได้ ส่วน `anotherSafeTask()` จัดการ Promise ผ่าน `.catch()` โดยตรง บล็อก try...catch รอบนอกไม่สามารถจับการ Reject ที่เกิดภายหลังจาก Promise ซึ่งไม่ได้ await ได้
9. การออกแบบข้อความแจ้งข้อผิดพลาดให้ปลอดภัย
ข้อความที่ใช้วิเคราะห์ปัญหาสำหรับนักพัฒนาไม่จำเป็นต้องเหมือนข้อความที่แสดงต่อผู้ใช้ทั่วไป ผู้ใช้ควรได้รับข้อความที่เข้าใจง่ายและบอกขั้นตอนถัดไปได้ ขณะที่รายละเอียดเชิงเทคนิคควรถูกบันทึกในระบบที่เข้าถึงได้เฉพาะผู้มีสิทธิ์
function getUserMessage(error) {
console.error('รายละเอียดสำหรับผู้ดูแล:', error);
return {
success: false,
message: 'ไม่สามารถดำเนินการได้ในขณะนี้ กรุณาลองอีกครั้ง'
};
}
try {
throw new Error('Database connection failed at internal host');
} catch (error) {
const response = getUserMessage(error);
console.log(response.message);
}
ตัวอย่างนี้แยกข้อความภายในที่ใช้วิเคราะห์ออกจากข้อความสำหรับผู้ใช้ ในระบบจริงควรส่งรายละเอียดไปยัง Logger ที่เหมาะสม ไม่ควรบันทึกรหัสผ่าน Token หรือข้อมูลส่วนบุคคลโดยไม่จำเป็น และไม่ควรแสดง Stack Trace รายละเอียดการเชื่อมต่อฐานข้อมูล หรือข้อมูลลับผ่านหน้าจอผู้ใช้
อย่ากลืนข้อผิดพลาดอย่างเงียบ ๆการจับข้อผิดพลาดแล้วไม่บันทึก ไม่แจ้งผล และไม่ส่งต่อ อาจทำให้ระบบดูเหมือนทำงานสำเร็จทั้งที่ข้อมูลไม่ได้ถูกบันทึก ควรเลือกอย่างชัดเจนว่าจะจัดการ แปลง ส่งต่อ หรือคืนผลลัพธ์ความล้มเหลวในรูปแบบใด
10. รูปแบบการจัดการข้อผิดพลาดที่ควรหลีกเลี่ยง
| แนวทางที่ไม่ดี | ปัญหาที่เกิดขึ้น | แนวทางที่เหมาะสมกว่า |
|---|---|---|
| จับทุกอย่างแล้วไม่ทำอะไร | ข้อผิดพลาดหายไปโดยไม่มีใครรู้ | บันทึก จัดการ หรือส่งต่ออย่างชัดเจน |
| แสดง Error ภายในให้ผู้ใช้ทั้งหมด | อาจเปิดเผยข้อมูลระบบ | ใช้ข้อความทั่วไปและบันทึกรายละเอียดอย่างปลอดภัย |
| ใช้ try...catch ครอบโค้ดทุกบรรทัด | ทำให้แยกสาเหตุและขอบเขตปัญหายาก | ครอบเฉพาะบริเวณที่มีวิธีตอบสนองต่อปัญหา |
| ใช้ return ใน finally โดยไม่เข้าใจผล | อาจกลบค่าคืนหรือข้อผิดพลาดเดิม | ใช้ finally สำหรับ cleanup เป็นหลัก |
| เรียก async โดยไม่จัดการ Promise | อาจเกิด Unhandled Rejection | ใช้ await กับ try...catch หรือ .catch() |
| จับ Error แล้วโยน Error ใหม่โดยไม่เก็บสาเหตุ | สูญเสียรายละเอียดต้นเหตุ | ส่งต่อ Error เดิมหรือเก็บสาเหตุด้วย cause เมื่อรองรับ |
11. ตัวอย่างประยุกต์: ระบบสร้างคำสั่งซื้อ
ลองนำแนวคิดมารวมกันในฟังก์ชันสร้างคำสั่งซื้อแบบง่าย โดยตรวจสอบข้อมูลสินค้า คำนวณยอดรวม และแยกข้อผิดพลาดจากข้อมูลไม่ถูกต้องออกจากข้อผิดพลาดที่ไม่คาดคิด ตัวอย่างนี้เป็นการสาธิตการทำงานในหน่วยความจำ ไม่ได้เชื่อมต่อฐานข้อมูลหรือระบบชำระเงินจริง
class OrderValidationError extends Error {
constructor(message) {
super(message);
this.name = 'OrderValidationError';
}
}
function createOrder(input) {
if (!input || typeof input !== 'object') {
throw new OrderValidationError('ข้อมูลคำสั่งซื้อไม่ถูกต้อง');
}
if (!Array.isArray(input.items) || input.items.length === 0) {
throw new OrderValidationError('กรุณาเลือกสินค้าอย่างน้อยหนึ่งรายการ');
}
const items = input.items.map(item => {
if (!item || !Number.isFinite(item.price) || item.price < 0) {
throw new OrderValidationError('ราคาสินค้าไม่ถูกต้อง');
}
if (!Number.isInteger(item.quantity) || item.quantity < 1) {
throw new OrderValidationError('จำนวนสินค้าต้องเป็นจำนวนเต็มบวก');
}
return {
name: String(item.name ?? 'สินค้า'),
price: item.price,
quantity: item.quantity,
subtotal: item.price * item.quantity
};
});
const total = items.reduce((sum, item) => sum + item.subtotal, 0);
return {
orderId: `ORD-${Date.now()}`,
items,
total,
status: 'pending'
};
}
try {
const order = createOrder({
items: [
{ name: 'สมุด', price: 45, quantity: 2 },
{ name: 'ปากกา', price: 12, quantity: 3 }
]
});
console.log('สร้างคำสั่งซื้อสำเร็จ:', order);
} catch (error) {
if (error instanceof OrderValidationError) {
console.log('ข้อมูลไม่ถูกต้อง:', error.message);
} else {
console.error('เกิดข้อผิดพลาดที่ไม่คาดคิด');
throw error;
}
}
ตัวอย่างนี้ใช้ Error Class เพื่อระบุปัญหาที่เกิดจากข้อมูลคำสั่งซื้อ และใช้ try...catch แยกการจัดการข้อผิดพลาดที่รู้จักออกจากข้อผิดพลาดที่ไม่คาดคิด อย่างไรก็ตาม ในระบบจริงต้องตรวจสอบราคาและสต็อกจากแหล่งข้อมูลที่เชื่อถือได้ ทำรายการแบบ Transaction เมื่อจำเป็น และตรวจสอบสิทธิ์กับข้อมูลบนเซิร์ฟเวอร์
12. แบบฝึกหัดท้ายบท
โจทย์ที่ 1: ตรวจสอบคะแนน
เขียนฟังก์ชัน `validateScore(score)` ให้โยน TypeError หากคะแนนไม่ใช่ตัวเลข และโยน RangeError หากคะแนนอยู่นอกช่วง 0 ถึง 100 หากถูกต้องให้คืนคะแนนเดิม
function validateScore(score) {
if (typeof score !== 'number' || !Number.isFinite(score)) {
throw new TypeError('คะแนนต้องเป็นตัวเลข');
}
if (score < 0 || score > 100) {
throw new RangeError('คะแนนต้องอยู่ระหว่าง 0 ถึง 100');
}
return score;
}
try {
console.log(validateScore(85));
console.log(validateScore(120));
} catch (error) {
console.log(error.name, error.message);
}
โจทย์ที่ 2: ใช้ finally คืนสถานะ
จำลองสถานะ `isLoading` โดยกำหนดเป็น true ก่อนเริ่มงาน และกำหนดกลับเป็น false ใน finally ไม่ว่างานจะสำเร็จหรือเกิดข้อผิดพลาด
let isLoading = false;
function runTask() {
isLoading = true;
try {
console.log('กำลังทำงาน:', isLoading);
throw new Error('เกิดปัญหาจำลอง');
} catch (error) {
console.log(error.message);
} finally {
isLoading = false;
console.log('สถานะสุดท้าย:', isLoading);
}
}
runTask();
โจทย์ที่ 3: จัดการ Promise ที่ถูก Reject
สร้างฟังก์ชัน async ที่รอ Promise ซึ่งอาจ Reject จากนั้นใช้ try...catch แสดงข้อความเมื่อเกิดปัญหา และใช้ finally เพื่อแจ้งว่ากระบวนการเสร็จสิ้น
function requestData() {
return Promise.reject(new Error('ไม่สามารถรับข้อมูลได้'));
}
async function main() {
try {
const data = await requestData();
console.log(data);
} catch (error) {
console.log('คำขอล้มเหลว:', error.message);
} finally {
console.log('จบการร้องขอข้อมูล');
}
}
main();
13. สรุปสิ่งที่ควรจำ
`try...catch` ช่วยจัดการข้อผิดพลาดที่เกิดขึ้นในขอบเขตที่ครอบไว้ `finally` เหมาะกับงานที่ต้องทำท้ายสุด `throw` ใช้แจ้งว่ามีสถานการณ์ที่ไม่สามารถดำเนินการต่อได้ และ Error Object ช่วยให้ตรวจสอบชื่อกับรายละเอียดของปัญหาได้ การสร้าง Error Class ช่วยแยกประเภทข้อผิดพลาดทางธุรกิจอย่างเป็นระบบ
สำหรับงานแบบ Async/Await ต้อง await Promise ภายใน try หากต้องการให้ try...catch จับการ Reject นั้นได้ หรือใช้ `.catch()` กับ Promise โดยตรง ควรตรวจสอบข้อมูลก่อนประมวลผล แสดงข้อความที่ปลอดภัยต่อผู้ใช้ และไม่กลืนข้อผิดพลาดที่ควรได้รับการจัดการหรือส่งต่อ
แนวทางปฏิบัติที่ดีตรวจสอบข้อมูลตั้งแต่ต้น จับข้อผิดพลาดในจุดที่สามารถตอบสนองได้จริง แยกข้อผิดพลาดที่คาดการณ์ได้จากข้อผิดพลาดที่ไม่คาดคิด บันทึกข้อมูลวิเคราะห์อย่างปลอดภัย และออกแบบผลลัพธ์เมื่อเกิดความล้มเหลวให้ชัดเจน
คำถามที่พบบ่อย
try...catch จับ Syntax Error ได้ทุกกรณีหรือไม่
ไม่ใช่ทุกกรณี Syntax Error ที่ทำให้ไฟล์หรือโค้ดทั้งส่วนไม่สามารถแยกวิเคราะห์ได้มักเกิดก่อนการทำงานของ try...catch ภายในโค้ดส่วนนั้น แต่ข้อผิดพลาดจากการแยกวิเคราะห์ข้อความที่ส่งให้ eval หรือ JSON.parse สามารถถูกจับได้หากคำสั่งนั้นทำงานภายใน try
ควรใช้ throw ข้อความหรือ new Error()
โดยทั่วไปควรใช้ new Error() หรือ Error Class ที่สืบทอดจาก Error เพื่อให้มี name, message และ stack ที่เป็นมาตรฐาน การโยนข้อความธรรมดาทำให้การตรวจสอบชนิดและรายละเอียดข้อผิดพลาดไม่สม่ำเสมอ
finally ทำงานเมื่อ try สำเร็จหรือไม่
โดยปกติ finally จะทำงานหลัง try และ catch ไม่ว่าจะเกิดข้อผิดพลาดหรือไม่ จึงเหมาะกับการคืนสถานะหรือทำความสะอาดทรัพยากร แต่ไม่ควรใช้ return ใน finally โดยไม่เข้าใจผลต่อค่าคืนและข้อผิดพลาดเดิม
ทำไม try...catch ไม่จับ Promise Reject บางครั้ง
หาก Promise ถูกสร้างขึ้นและ Reject ภายหลังโดยไม่ได้ await ภายใน try หรือไม่ได้เชื่อมต่อ .catch() การ Reject นั้นจะไม่ถูกจับด้วย try...catch แบบซิงโครนัส ต้องใช้ await หรือจัดการ Promise ด้วย .catch()
ควรแสดง error.message ให้ผู้ใช้เห็นหรือไม่
ขึ้นอยู่กับเนื้อหาของข้อความ สำหรับข้อผิดพลาดที่ผู้ใช้แก้ไขได้ เช่น กรอกข้อมูลไม่ครบ อาจแสดงข้อความที่เหมาะสมได้ แต่รายละเอียดภายใน เช่น Stack Trace ชื่อเครื่องภายใน หรือข้อมูลการเชื่อมต่อ ควรถูกจำกัดไว้ในระบบบันทึกที่ปลอดภัย
โปรแกรมที่เชื่อถือได้ไม่ใช่โปรแกรมที่ไม่เคยผิดพลาด แต่คือโปรแกรมที่รู้ว่าจะตอบสนองต่อความผิดพลาดอย่างไร
เตรียมพบ EP16
ตอนต่อไปจะเจาะลึก JavaScript Promise และ Async/Await ตั้งแต่สถานะ Pending, Fulfilled และ Rejected การใช้ then, catch และ finally ไปจนถึง Promise.all(), Promise.allSettled() และการจัดลำดับงานแบบอะซิงโครนัส เพื่อให้คุณสร้างโปรแกรมที่ติดต่อ API และจัดการงานที่ใช้เวลาได้อย่างมั่นใจ
Reach within 500px of the page end and stay for at least 5 minutes to earn EXP once per day.
⏳ Reading timer is running...