JavaScript
Error Handling
ข้อควรระวังและแนวปฏิบัติที่ดีกับ Error
หลีกเลี่ยง silent catch, throw string, และ error message ที่ไม่สื่อ — เขียน error handling ให้ปลอดภัยและแก้ bug ง่าย
อย่า swallow error — catch ที่ว่างเปล่าเป็นหลุมพรางร้ายแรงที่สุด
ข้อผิดพลาดร้ายแรงที่สุดในการเขียน error handling ไม่ใช่การเขียน `try...catch` ผิด syntax — แต่คือการเขียน `catch` ที่**ว่างเปล่า** catch ว่างเปล่า หมายถึง `catch(err) { }` หรือ `catch(err) { /* ไม่ทำอะไรเลย */ }` — เราเรียกพฤติกรรมนี้ว่า **swallow error** (กลืน error) เพราะ error ถูกดักจับแล้วโยนทิ้ง ไม่มีใครรู้ว่าเกิดอะไรขึ้น ทำไม swallow error ถึงอันตราย: - โปรแกรมทำงานต่อเหมือนไม่มีอะไรเกิด — แต่** logic อาจผิดพลาด** โดยไม่มีใครรู้ - **หาบัคแทบไม่เจอ** — เพราะไม่มี console error, ไม่มี stack trace, ไม่มีร่องรอยอะไรเลย - ผู้ใช้เห็นผลลัพธ์ที่ผิด แต่ไม่มีข้อความแจ้งเตือน — แย่กว่าการ crash เพราะ crash อย่างน้อยก็รู้ว่ามีปัญหา **กฎเหล็กข้อที่ 1 ของ error handling**: ทุก catch ต้องทำอะไรบางอย่าง — อย่างน้อยที่สุดคือ `console.error(err)`
catch ว่างเปล่าทำให้โปรแกรมทำงานต่อเหมือนไม่มีอะไรเกิด — แต่ข้อมูลผิดโดยไม่มีใครรู้
// ❌ BAD: catch ว่างเปล่า — error ถูก "กลืน" หายไป
const userSettings = { theme: "dark" };
// สังเกต: ไม่มี property .notifications — settings.notifications เป็น undefined
function applySettings(settings) {
try {
if (settings.notifications.email) {
console.log("เปิด email notification แล้ว");
}
} catch (err) {
// ❌ ไม่ทำอะไรเลย — error หายไปเงียบ ๆ
// TypeError: Cannot read properties of undefined (reading 'email')
// → แต่ไม่มีใครรู้ เพราะ catch ว่างเปล่า
}
console.log("ตั้งค่า theme เป็น:", settings.theme);
}
applySettings(userSettings);
// output: "ตั้งค่า theme เป็น: dark"
// ^ ดูเหมือนทุกอย่างปกติ... แต่ notification ไม่ถูกตั้งค่ากฎขั้นต่ำ: ทุก catch ต้องมี console.error — developer จะได้รู้ว่ามี bug
// ✅ GOOD: ขั้นต่ำ — log error ไว้
function applySettings(settings) {
try {
if (settings.notifications.email) {
console.log("เปิด email notification แล้ว");
}
} catch (err) {
// ✅ log error — developer รู้ว่ามี bug
console.error("ตั้งค่า notification ไม่สำเร็จ:", err.message);
// ✅ fallback — ใช้ค่า default
console.log("ใช้ค่า default สำหรับ notification แทน");
}
console.log("ตั้งค่า theme เป็น:", settings.theme);
}
applySettings(userSettings);
// output:
// "ตั้งค่า notification ไม่สำเร็จ: Cannot read properties of undefined (reading 'email')"
// "ใช้ค่า default สำหรับ notification แทน"
// "ตั้งค่า theme เป็น: dark"
// ^ developer เห็น error → แก้ได้ + ผู้ใช้ได้ค่า default → โปรแกรมไม่พัง- **Swallow error** = `catch(err) { }` ว่างเปล่า — error ถูกดักจับแล้วทิ้ง ไม่มีใครรู้
- Swallow error อันตรายที่สุด — bug ถูกซ่อน หายากกว่า crash ที่อย่างน้อยก็เห็น error ใน console
- **กฎขั้นต่ำ**: ทุก `catch` ต้องมี `console.error(err)` หรือ `console.log(err.message)` — ต้องมีร่องรอยว่าเกิด error
- **กฎที่ดีกว่า**: log error + fallback (ใช้ค่า default) + แจ้งผู้ใช้ — ไม่ใช่แค่ log แล้วปล่อยผ่าน
- ก่อนเขียน `catch(err) { }` — ถามตัวเอง: ถ้า error เกิดแล้วใครจะรู้?
throw Error object เสมอ — อย่าโยน string หรือ number
คุณรู้แล้วว่า `throw` ใช้ได้กับค่าอะไรก็ได้ — string, number, object — แต่นั่นไม่ได้หมายความว่าควรทำ ในโค้ดจริงที่มีหลายร้อย function — การโยน string แทน Error object ทำให้ **debug แทบไม่ได้** เปรียบเทียบสิ่งที่คุณได้: | | `throw new Error(...)` | `throw "ข้อความ"` | |---|---|---| | `.name` — บอกประเภท error | ✅ `"Error"`, `"TypeError"`, ฯลฯ | ❌ ไม่มี | | `.message` — บอกรายละเอียด | ✅ ข้อความที่เราใส่ | ❌ ไม่มี (ตัว string คือค่าทั้งก้อน) | | `.stack` — stack trace | ✅ บอก function และบรรทัดที่เกิด error | ❌ ไม่มี | | ใช้ `err.name` แยกประเภท | ✅ แยกการจัดการตามประเภทได้ | ❌ ทำไม่ได้ | | `console.log(err)` แสดงข้อมูลครบ | ✅ | ❌ แสดงแค่ string | พูดสั้น ๆ: `throw new Error(...)` ให้ข้อมูล debug ครบ — `throw "ข้อความ"` ให้ข้อมูลแทบไม่มีเลย
เมื่อหลาย function โยน string — ดู error แล้วไม่รู้ว่าใครเป็นต้นเหตุ
// ❌ BAD: หลาย function โยน string — error เกิดจากใครไม่รู้
function validateName(name) {
if (name.length < 3) throw "ชื่อสั้นเกินไป";
}
function validateAge(age) {
if (age < 0) throw "อายุติดลบ";
}
function validateEmail(email) {
if (!email.includes("@")) throw "email ไม่มี @";
}
function registerUser(name, age, email) {
validateName(name);
validateAge(age);
validateEmail(email);
console.log("ลงทะเบียนสำเร็จ");
}
try {
registerUser("a", -5, "bad-email");
} catch (err) {
console.log("เกิด error:", err);
// output: "เกิด error: ชื่อสั้นเกินไป"
// ^ แค่ string — ไม่รู้ว่าเกิดที่ function validateName
// ไม่รู้บรรทัดไหน — ไม่รู้ว่า validateName ถูกเรียกจาก registerUser
// ถ้า validateName() ถูกเรียกจาก 10 ที่ — หาจุดที่เรียกไม่เจอ
}เปลี่ยนจาก string เป็น Error object — ข้อมูล debug ครบถ้วน หาจุด error ได้ทันที
// ✅ GOOD: โยน Error object — ข้อมูล debug ครบ
function validateName(name) {
if (name.length < 3) {
throw new Error("ชื่อต้องมีอย่างน้อย 3 ตัว — แต่ได้รับ " + name.length + " ตัว (" + name + ")");
}
}
function validateAge(age) {
if (age < 0) {
throw new RangeError("อายุต้อง >= 0 — แต่ได้รับ " + age);
}
}
function validateEmail(email) {
if (!email.includes("@")) {
throw new Error("email ต้องมี @ — แต่ได้รับ: " + email);
}
}
function registerUser(name, age, email) {
validateName(name);
validateAge(age);
validateEmail(email);
console.log("ลงทะเบียนสำเร็จ");
}
try {
registerUser("a", -5, "bad-email");
} catch (err) {
console.log("ประเภท:", err.name); // "Error"
console.log("ข้อความ:", err.message);
// "ชื่อต้องมีอย่างน้อย 3 ตัว — แต่ได้รับ 1 ตัว (a)"
console.log("stack trace:", err.stack);
// Error: ชื่อต้องมีอย่างน้อย 3 ตัว...
// at validateName (description.ts:5)
// at registerUser (description.ts:15)
// at ...
// ^ รู้ทันที: error เกิดใน validateName → ถูกเรียกจาก registerUser
}err.name บอกประเภท error — ใช้เลือกวิธีจัดการต่างกันได้ — ฟีเจอร์นี้ใช้ไม่ได้เลยถ้าโยน string
// ✅ Error object → ตรวจ err.name เพื่อจัดการต่างกัน
function processInput(input) {
try {
if (typeof input !== "string") {
throw new TypeError("input ต้องเป็น string — แต่ได้รับ " + typeof input);
}
if (input.trim() === "") {
throw new Error("input ห้ามเป็นค่าว่าง");
}
console.log("ประมวลผล:", input);
} catch (err) {
if (err.name === "TypeError") {
console.log("❌ type ไม่ถูกต้อง — กรุณาส่ง string มาเท่านั้น");
} else if (err.name === "Error") {
console.log("❌ ข้อมูลไม่ถูกต้อง:", err.message);
} else {
console.log("❌ error อื่น ๆ:", err.message);
}
}
}
processInput(123); // TypeError → "❌ type ไม่ถูกต้อง..."
processInput(""); // Error → "❌ ข้อมูลไม่ถูกต้อง..."
// ^ แต่ละประเภท error ได้ข้อความตอบสนองต่างกัน
// ถ้าโยน string — แยกแบบนี้ไม่ได้เลย- **โยน `Error` object เสมอ** — `throw new Error(...)` หรือ subtype (`TypeError`, `RangeError`) — ห้ามโยน string, number, หรือค่าดิบอื่น ๆ
- Error object มี **`.name`** (ประเภท), **`.message`** (รายละเอียด), **`.stack`** (stack trace — บอก function และบรรทัดที่เกิด error)
- `throw "ข้อความ"` ดูเหมือนสั้นและง่าย — แต่พอโปรแกรมมีหลายร้อย function คุณจะเสียเวลา debug เป็นชั่วโมง
- `.name` ทำให้แยกประเภท error ใน `catch` ได้ (`if (err.name === "TypeError")`) — มันคือข้อมูลที่ string ให้คุณไม่ได้
- Subtype ให้เหมาะกับสถานการณ์: `TypeError` สำหรับ type ผิด, `RangeError` สำหรับค่าอยู่นอกช่วง, `Error` สำหรับข้อผิดพลาดทั่วไป
เขียน error message ให้อ่านแล้วรู้ทันทีว่าอะไรผิดและต้องแก้ยังไง
เวลา error เกิดขึ้น — error message คือข้อมูล**เพียงอย่างเดียว**ที่ developer (หรือตัวคุณในอีก 3 เดือนข้างหน้า) จะได้เห็นเพื่อ debug error message ที่ดีตอบ 3 คำถาม: 1. **เกิดอะไรขึ้น** — ปัญหาคืออะไร (ไม่ใช่แค่ "Error" หรือ "fail") 2. **ค่าอะไรที่ทำให้เกิดปัญหา** — input หรือค่าจริงที่ trigger error คืออะไร 3. **ควรแก้ยังไง** — ค่าที่ถูกต้องควรเป็นอะไร หรือต้องทำอะไรถึงจะผ่าน error message ที่แย่ — เช่น `"Error"`, `"Invalid"`, `"fail"` — บอกแค่ว่า "มีปัญหา" โดยไม่บอกว่าเป็นปัญหาอะไร ทำให้เสียเวลาไล่หาสาเหตุ
| ❌ ข้อความที่ไม่สื่อ | ปัญหา | ✅ ข้อความที่สื่อความหมาย |
|---|---|---|
| `"Error"` | ไม่บอกอะไรเลย — ไม่รู้ว่า error อะไร | `"ไม่สามารถบันทึกข้อมูลผู้ใช้ได้ — username 'somchai' ซ้ำในฐานข้อมูล"` |
| `"Invalid input"` | ไม่บอกว่าอะไร invalid — ต้องไล่ debug ต่อ | `"age ต้องเป็น number ระหว่าง 0-150 — แต่ได้รับ -5"` |
| `"fail"` | ไม่บอกว่าล้มเหลวเพราะอะไร — fail ได้ร้อยเหตุผล | `"เชื่อมต่อ API ไม่สำเร็จ — timeout หลังจากรอ 5 วินาที"` |
| `"type error"` | type error กับอะไร — string? number? field ไหน? | `"price ต้องเป็น number — แต่ได้รับ string '100 บาท'"` |
| `"ไม่พบ"` | ไม่พบอะไร — ไฟล์? ผู้ใช้? ข้อมูล? | `"ไม่พบผู้ใช้ที่มี id 'USR-1234' ในฐานข้อมูล"` |
เวลา error เกิดใน production — ข้อความแบบนี้ทำให้เสียเวลาเป็นชั่วโมงในการไล่หาสาเหตุ
// ❌ BAD: error message ที่ไม่สื่อ — debug ยาก
function createUser(data) {
if (!data.name) {
throw new Error("Error"); // ← อะไร error? ไม่รู้
}
if (typeof data.age !== "number") {
throw new Error("Invalid"); // ← อะไร invalid? ไม่รู้
}
if (!data.email.includes("@")) {
throw new Error("fail"); // ← ล้มเหลวเพราะอะไร? ไม่รู้
}
if (data.password.length < 8) {
throw new Error("Error"); // ← อีกแล้ว — error อะไร?
}
console.log("สร้างผู้ใช้สำเร็จ");
}
// เวลา error เกิดใน console เราเห็นแค่:
// Error: Error ← ต้องเปิดโค้ดดูบรรทัดที่ throw
// Error: Invalid ← ต้องเปิดโค้ดดูอีก
// Error: fail ← เปิดโค้ดอีก — เสียเวลาไล่ทีละ throwทุก error message บอก: เกิดอะไรขึ้น + ได้รับค่าอะไร + ควรเป็นอะไร
// ✅ GOOD: error message ที่สื่อความหมาย — debug เร็ว ไม่ต้องเปิดโค้ดดู
function createUser(data) {
if (!data.name) {
throw new Error("name ห้ามเป็นค่าว่าง — กรุณากรอกชื่อผู้ใช้");
}
if (typeof data.age !== "number") {
throw new TypeError(
"age ต้องเป็น number — แต่ได้รับ " + typeof data.age + " (" + data.age + ")"
);
}
if (!data.email.includes("@")) {
throw new Error(
"email รูปแบบไม่ถูกต้อง — ต้องมี @ — แต่ได้รับ: " + data.email
);
}
if (data.password.length < 8) {
throw new Error(
"password ต้องมีอย่างน้อย 8 ตัวอักษร — แต่ได้รับ " + data.password.length + " ตัว"
);
}
console.log("สร้างผู้ใช้สำเร็จ:", data.name);
}
// เวลา error เกิด เราเห็น:
// Error: name ห้ามเป็นค่าว่าง — กรุณากรอกชื่อผู้ใช้
// TypeError: age ต้องเป็น number — แต่ได้รับ string (25)
// Error: email รูปแบบไม่ถูกต้อง — ต้องมี @ — แต่ได้รับ: somchai.gmail.com
// Error: password ต้องมีอย่างน้อย 8 ตัวอักษร — แต่ได้รับ 5 ตัว
// ^ อ่าน message แล้วรู้ทันที — ไม่ต้องเปิดโค้ดไล่- **error message ที่ดีตอบ 3 คำถาม**: เกิดอะไรขึ้น / ได้รับค่าอะไร / ควรเป็นอะไร
- **ใส่ค่าจริงที่ทำให้เกิด error ลงใน message**: `"...แต่ได้รับ " + value` — ทำให้เห็นทันทีว่า input คืออะไร
- **บอกวิธีแก้ใน message**: `"กรุณากรอก..."`, `"ต้องมีอย่างน้อย..."`, `"รองรับเฉพาะ..."` — developer ไม่ต้องเดา
- **อย่าใช้คำกำกวม**: `"Error"`, `"fail"`, `"Invalid"`, `"ไม่พบ"` — คำพวกนี้บังคับให้เปิดโค้ดดูทุกครั้ง
- error message ที่ดีใช้เวลาคิดเพิ่ม 10 วินาที — แต่ประหยัดเวลา debug เป็นชั่วโมง
catch ให้เฉพาะเจาะจง — อย่าครอบ try block กว้างเกินไป
ข้อผิดพลาดที่พบบ่อยอีกอย่างคือการเขียน `try` block ใหญ่เกินไป — ครอบหลาย operation ที่ไม่เกี่ยวข้องกันไว้ใน `try` เดียวกัน ปัญหาที่ตามมา: - **ไม่รู้ว่า error เกิดจากไหน** — `try` มี 5 operation → error มาจาก operation ไหน? ต้องไล่ debug ทีละบรรทัด - **จัดการต่างกันไม่ได้** — `fetchUser` fail กับ `sendEmail` fail ควรตอบสนองต่างกัน — แต่ `catch` เดียวจัดการทุกอย่างเหมือนกัน - **error ที่ไม่ควรถูก catch ก็ถูก catch** — `saveLog()` และ `updateLastLogin()` ไม่น่า fail เลย — ถ้ามัน fail จริง ๆ เราควรรู้ ไม่ใช่ให้ catch ซ่อนมัน **หลักการ**: `try` block ควรเล็ก — ครอบเฉพาะโค้ดที่อาจเกิด error จริง ๆ (1-3 บรรทัด) — และแต่ละ `catch` ควรตรวจ `err.name` เพื่อจัดการเฉพาะ error ที่รู้วิธีรับมือ โค้ดที่ไม่ควร fail — อย่าใส่ใน `try` — เพราะถ้ามัน fail จริง ๆ นั่นคือ bug ที่คุณต้องเห็น
เมื่อ try ครอบหลาย operation — error จาก operation ไหนก็ไม่รู้ จัดการได้แค่แบบเดียว
// ❌ BAD: try block ใหญ่ — 6 บรรทัดใน try เดียว
function setupUserProfile(userId) {
try {
let user = loadUser(userId); // อาจ fail: user ไม่พบ
let config = JSON.parse(user.settings); // อาจ fail: JSON เสีย
let theme = config.theme.color; // อาจ fail: property ไม่มี
saveActivityLog("user login", userId); // ไม่ควร fail เลย
sendWelcomeMessage(user.email, "ยินดีต้อนรับ"); // อาจ fail: network
updateLastLogin(userId); // ไม่ควร fail เลย
console.log("ตั้งค่าโปรไฟล์สำเร็จ");
} catch (err) {
// ❌ error เกิดจาก loadUser? JSON.parse? sendWelcomeMessage?
// ไม่รู้ — จัดการเหมือนกันหมดด้วย catch เดียว
console.log("เกิดข้อผิดพลาด — กรุณาลองใหม่");
}
}แยก try ตาม operation — แต่ละ catch จัดการเฉพาะของ operation นั้น — error ที่ไม่รู้จัก → re-throw
// ✅ GOOD: try block เล็ก — แต่ละ operation มี try...catch ของตัวเอง
function setupUserProfile(userId) {
// 1. Load user — try เฉพาะ loadUser
let user;
try {
user = loadUser(userId);
} catch (err) {
console.error("โหลดข้อมูลผู้ใช้ไม่สำเร็จ:", err.message);
return; // หยุด — ขาดข้อมูลหลักไปต่อไม่ได้
}
// 2. Parse config — try เฉพาะ JSON.parse
let config;
try {
config = JSON.parse(user.settings);
} catch (err) {
if (err.name === "SyntaxError") {
console.log("config ผิดรูปแบบ — ใช้ค่า default");
config = { theme: { color: "light" } };
} else {
throw err; // error อื่นที่ parse JSON ไม่ควรเกิด — re-throw
}
}
// 3. งานที่ไม่ควร fail — อยู่นอก try
saveActivityLog("user login", userId);
updateLastLogin(userId);
// 4. Send message — try เฉพาะ sendWelcomeMessage
try {
sendWelcomeMessage(user.email, "ยินดีต้อนรับ");
} catch (err) {
console.log("ส่งข้อความต้อนรับไม่สำเร็จ — แต่ไม่กระทบการทำงานหลัก");
// ไม่หยุด — ข้อความเป็นแค่ส่วนเสริม
}
console.log("ตั้งค่าโปรไฟล์สำเร็จ — theme:", config.theme.color);
}- **try block ควรเล็ก** — ครอบเฉพาะโค้ดที่อาจเกิด error จริง ๆ (1-3 บรรทัด) — ไม่ใช่ครอบครึ่ง function
- **แต่ละ operation ที่อาจ fail ควรมี try ของตัวเอง** — `loadUser` fail → จัดการแบบหนึ่ง, `sendMessage` fail → จัดการอีกแบบ
- **โค้ดที่ไม่ควร fail อย่าใส่ใน try** — ถ้า `saveLog()` fail จริง ๆ นั่นคือ bug ที่คุณต้องเห็น ไม่ใช่ให้ catch ซ่อนมัน
- **ตรวจ `err.name` ใน catch**: `SyntaxError` → ใช้ fallback ได้, `TypeError` → จัดการอีกแบบ, error อื่นที่เราไม่รู้จัก → re-throw
- หลักการ: รู้ว่าอะไรอาจ fail → try เล็ก, ไม่รู้ว่า fail ได้ไง → bug ต้องโผล่ให้เห็น
try/catch มีไว้สำหรับ unexpected error — ใช้ if/else สำหรับเงื่อนไขที่คาดการณ์ได้
หนึ่งในความเข้าใจผิดที่พบบ่อยที่สุดคือการใช้ `try...catch` เหมือนเป็นเครื่องมือควบคุม flow — ใช้แทน `if/else` ในสถานการณ์ที่สามารถตรวจสอบเงื่อนไขล่วงหน้าได้ หลักการแยกแยะ: - **`if/else`** — ใช้เมื่อ**คาดการณ์ได้**ว่าจะเกิดเงื่อนไขผิดพลาด: null check, type check (`typeof`), range check (`<`, `>`), empty string, missing property - **`try...catch`** — ใช้เมื่อ**คาดการณ์ไม่ได้**ว่า error จะเกิดเมื่อไหร่: `JSON.parse` กับข้อมูลจากภายนอก, network request, การอ่านไฟล์, third-party library ทำไมไม่ควรใช้ try/catch แทน if/else: 1. **Performance** — try/catch มี overhead มากกว่า if/else (engine ต้องเตรียมโครงสร้างสำหรับ stack unwinding) 2. **อ่านยาก** — โค้ดที่ใช้ throw/catch เป็น flow control อ่านยากกว่า if/else ตรง ๆ 3. **บดบัง logic** — ซ่อนเงื่อนไขทางธุรกิจไว้ใน catch แทนที่จะแสดงชัดเจน 4. **เสี่ยง swallow** — catch อาจดัก error ที่เราไม่ได้ตั้งใจจะดัก
throw/catch สำหรับเงื่อนไขที่ตรวจด้วย if ล่วงหน้าได้ — โค้ดช้าและอ่านยาก
// ❌ BAD: ใช้ try...catch แทนการตรวจสอบแบบ if/else
function parseAge(input) {
try {
let age = Number(input);
if (isNaN(age)) {
throw new Error("ไม่ใช่ตัวเลข"); // ❌ ใช้ throw แทน if/else
}
if (age < 0 || age > 150) {
throw new Error("อายุอยู่นอกช่วง"); // ❌ ใช้ throw แทน if/else
}
return age;
} catch (err) {
// ❌ catch กลายเป็น "else" ของทุกเงื่อนไข — ซ่อน logic
console.log("ข้อมูลอายุไม่ถูกต้อง:", err.message);
return null;
}
}
console.log(parseAge("25")); // 25 ✅
console.log(parseAge("abc")); // null — ต้องผ่าน throw/catch ถึงรู้ว่าผิด
console.log(parseAge("-5")); // null — ต้องผ่าน throw/catch อีกเงื่อนไขทุกอย่างตรวจด้วย if ล่วงหน้าได้ — ไม่ต้องพึ่ง try/catch — โค้ดอ่านง่ายและเร็วกว่า
// ✅ GOOD: ใช้ if/else — เงื่อนไขที่ตรวจได้ล่วงหน้า ไม่ต้องใช้ try...catch
function parseAge(input) {
let age = Number(input);
if (isNaN(age)) {
console.log("ข้อมูลอายุไม่ถูกต้อง: input ไม่ใช่ตัวเลข — ได้รับ", input);
return null;
}
if (age < 0 || age > 150) {
console.log("ข้อมูลอายุไม่ถูกต้อง: อายุต้องอยู่ระหว่าง 0-150 — แต่ได้รับ", age);
return null;
}
return age;
}
console.log(parseAge("25")); // 25
console.log(parseAge("abc")); // null + log ชัดเจน
console.log(parseAge("-5")); // null + log ชัดเจน
// ✅ try...catch ใช้เมื่อคาดการณ์ไม่ได้จริง ๆ — เช่นข้อมูลจากภายนอก
function parseRemoteData(rawData) {
try {
return JSON.parse(rawData);
// rawData มาจาก API / ไฟล์ / ผู้ใช้ — เราควบคุม format ไม่ได้
} catch (err) {
console.log("ข้อมูลจากภายนอกผิดรูปแบบ:", err.message);
return null;
}
}- **`if/else` ใช้เมื่อตรวจล่วงหน้าได้**: null check, `typeof`, range check (`<`, `>`), empty string, undefined property — เงื่อนไขที่เรา**รู้ว่าอาจเกิด**
- **`try...catch` ใช้เมื่อคาดการณ์ไม่ได้**: `JSON.parse` กับข้อมูลจากภายนอก, network request, อ่านไฟล์, third-party library — สิ่งที่เรา**ควบคุมไม่ได้**
- ถามตัวเองก่อนใช้ try/catch: "เงื่อนไขนี้ตรวจด้วย `if` ล่วงหน้าได้ไหม?" ถ้าได้ — ใช้ if
- try/catch ไม่ได้ทำให้โค้ดปลอดภัยขึ้นถ้าใช้ผิดที่ — มันทำให้ bug ซ่อนตัวลึกขึ้นเพราะทุก error ถูก catch ไว้
Log ก่อน re-throw เสมอ — อย่าปล่อยให้ error หายไปอย่างไร้ร่องรอย
คุณรู้แล้วว่า re-throw (`throw err` ใน catch) คือการส่ง error ต่อไปให้ caller จัดการ — แต่กฎที่สำคัญไม่แพ้กันคือ: **ก่อน re-throw ต้อง log หรือเพิ่ม context ให้ error เสมอ** การ re-throw เปล่า ๆ (`catch(err) { throw err; }`) ไม่มีประโยชน์เลย — มันเหมือนกับการไม่เขียน try...catch ตั้งแต่แรก ถ้าลบ try...catch ออกแล้วโปรแกรมทำงานเหมือนเดิม — นั่นคือสัญญาณว่า catch นั้นไร้ประโยชน์ สิ่งที่ควรทำก่อน re-throw: 1. **Log ข้อมูล error** — `console.error(err)` พร้อม context ว่า function นี้กำลังทำอะไรอยู่ 2. **เพิ่ม context** — ใส่ข้อมูลเฉพาะ domain เช่น "กำลังโหลด config ของ user USR-123" 3. **ส่งต่อ** — re-throw เพื่อให้ caller ตัดสินใจว่าจะจัดการยังไง จำกฎง่าย ๆ: ถ้าคุณจัดการ error ได้ — จัดการเลย (log + fallback) ถ้าจัดการไม่ได้ — log แล้ว re-throw
catch แล้ว re-throw ทันทีโดยไม่ทำอะไร — เขียน try...catch เปล่า ๆ เสียเวลา
// ❌ BAD: Re-throw เปล่า — ไม่ log, ไม่มี context, จับแล้วโยนต่อเปล่า ๆ
function parseAppConfig(rawConfig) {
try {
return JSON.parse(rawConfig);
} catch (err) {
throw err; // ❌ ไม่ log, ไม่เพิ่มข้อมูล — catch นี้ไร้ประโยชน์
}
// ^ ลบ try...catch ออกทั้งก้อน — โปรแกรมทำงานเหมือนเดิม
// ถ้า catch มีค่า — มันต้องทำอะไรบางอย่าง
}
// ❌ BAD: Log แบบไม่มีข้อมูล — แค่ "error" ไม่ช่วยอะไร
function processOrder(data) {
try {
return calculateTotal(data);
} catch (err) {
console.log("error"); // ❌ แค่ "error" — error อะไร? ข้อมูลอะไร?
throw err;
}
}log ข้อมูลให้มากที่สุด: error type, message, ข้อมูลที่กำลังประมวลผล, timestamp
// ✅ GOOD: Log พร้อม context เต็มที่ก่อน re-throw
function parseAppConfig(rawConfig, source) {
try {
return JSON.parse(rawConfig);
} catch (err) {
// ✅ log ข้อมูลครบ — รู้ทันทีว่าเกิดอะไรขึ้น ตอนไหน กับข้อมูลอะไร
console.error("[parseAppConfig] parse config ไม่สำเร็จ:", {
source: source, // config มาจากไฟล์ไหน
errorType: err.name, // SyntaxError? TypeError?
message: err.message, // ข้อความจาก Error object
time: new Date().toISOString() // เกิดตอนไหน
});
throw err; // ส่งต่อให้ caller — แต่เรามี log แล้ว
}
}
// ✅ GOOD: เพิ่ม context แบบ domain-specific ก่อน re-throw
function loadUserProfile(userId) {
try {
let rawData = loadRawFile("users/" + userId + ".json");
return JSON.parse(rawData);
} catch (err) {
// ✅ เพิ่ม context: user คนไหน, มาจากขั้นตอนไหน
console.error("[loadUserProfile] โหลดโปรไฟล์ไม่สำเร็จ — userId:", userId, "|", err.message);
throw new Error(
"ไม่สามารถโหลดโปรไฟล์ของผู้ใช้ " + userId + " ได้ — " + err.message
);
// ^ caller เห็น: "ไม่สามารถโหลดโปรไฟล์ของผู้ใช้ USR-123 ได้ — ..."
// แทนที่จะเห็นแค่: "Unexpected token 'x'..." ซึ่งไม่บอกเลยว่า user คนไหน
}
}- **Re-throw เปล่า (`throw err`) ไม่ต่างจากไม่มี try...catch** — ถ้าลบ catch แล้วโปรแกรมทำงานเหมือนเดิม แสดงว่า catch นั้นไร้ค่า
- **Log ให้มีประโยชน์**: error type (`err.name`), error message, ข้อมูลที่กำลังทำอยู่, timestamp — ไม่ใช่แค่ `console.log('error')`
- **เพิ่ม context**: ผู้ใช้คนไหน, ไฟล์อะไร, ขั้นตอนไหน — ข้อมูลพวกนี้ไม่มีใน Error object เดิม — คุณต้องเพิ่มเอง
- **กฎง่าย ๆ**: จัดการได้ → จัดการเลย (log + fallback) จัดการไม่ได้ → log แล้ว re-throw
- **อย่า log ซ้ำซ้อน**: ถ้า function A re-throw และ function B จัดการ — log ที่ B ก็พอ (ไม่งั้น console จะ spam)
บทสรุป: 7 Antipattern กับแนวปฏิบัติที่ถูกต้อง
ตลอดบทนี้เราได้เห็น antipattern ที่พบบ่อยในการเขียน error handling — นี่คือตารางสรุปทั้งหมด:
| ❌ Antipattern | 🚫 ผลเสีย | ✅ Best Practice |
|---|---|---|
| catch ว่างเปล่า `catch(err) { }` | Error ถูกกลืน — bug ซ่อนตัว — หาไม่เจอ — โปรแกรมทำงานต่อแบบข้อมูลผิด | อย่างน้อย `console.error(err)` — ที่ดีกว่าคือ log + fallback + แจ้งผู้ใช้ |
| `throw "ข้อความ"` — โยน string | ไม่มี `.name` `.stack` — debug ไม่ได้ — แยกประเภท error ใน catch ไม่ได้ | `throw new Error("ข้อความ")` หรือ subtype (`TypeError`, `RangeError`) |
| error message ไม่สื่อ: "Error", "fail", "Invalid" | เสียเวลา debug เป็นชั่วโมง — ไม่รู้ว่าอะไรผิด ต้องเปิดโค้ดดูทุกครั้ง | บอก: เกิดอะไรขึ้น + ได้รับค่าอะไร + ควรแก้ยังไง |
| try block กว้างเกินไป — 6+ บรรทัดใน try เดียว | ไม่รู้ว่า error เกิดจากบรรทัดไหน — จัดการต่างกันไม่ได้ — ซ่อน bug | try block เล็ก (1-3 บรรทัด) — ครอบเฉพาะโค้ดที่อาจ fail — แยก try ต่อ operation |
| ใช้ try/catch แทน if/else — flow control ผ่าน exception | ช้ากว่า — อ่านยาก — ซ่อน logic จริง — เสี่ยง swallow error ที่ไม่ตั้งใจ | if/else สำหรับเงื่อนไขที่คาดการณ์ได้ — try/catch สำหรับสิ่งที่ควบคุมไม่ได้ |
| Re-throw เปล่า `throw err` โดยไม่ log หรือเพิ่ม context | ไม่มีการบันทึก error — ถ้าไม่มีใครจัดการ error จะหายไปเงียบ ๆ | Log พร้อม context ก่อน re-throw — `console.error({err, context})` |
| catch แล้วไม่ตรวจ `err.name` — จัดการทุก error เหมือนกันหมด | SyntaxError, TypeError, Error จัดการเหมือนกัน — ตอบสนองไม่ตรง — เสี่ยงซ่อนปัญหาร้ายแรง | `if (err.name === "TypeError")` → แยกการจัดการตามประเภท — error ที่ไม่รู้จัก → re-throw |
**หลัก 3 ข้อที่ต้องจำขึ้นใจ**: 1. **ทุก catch ต้องทำอะไรบางอย่าง** — อย่างน้อย `console.error(err)` — ไม่มี catch ไหนควรว่างเปล่า 2. **always throw Error (ไม่ใช่ string)** — Error object ให้ข้อมูล debug ที่จำเป็นเมื่อโค้ดมีเป็นร้อย function 3. **try/catch มีไว้สำหรับสิ่งที่ควบคุมไม่ได้** — เงื่อนไขที่ตรวจล่วงหน้าได้ ใช้ `if/else` ให้หมด ถ้าทำตาม 3 ข้อนี้ — error handling ของคุณจะปลอดภัย แก้บัคง่าย และไม่เป็นภาระให้ทีมในอนาคต
- **Don't swallow**: catch ว่างเปล่าคือศัตรูอันดับ 1 — ทุก catch ต้องมี `console.error`
- **Don't throw string**: โยน `new Error()` หรือ subtype เสมอ — `.name` `.message` `.stack` มีค่ามหาศาลตอน debug
- **Don't be vague**: error message ต้องตอบ: อะไรผิด / ได้รับค่าอะไร / ควรแก้ยังไง
- **Don't over-broad**: try block ควรเล็ก — ครอบเฉพาะสิ่งที่อาจ fail — งานที่ไม่ควร fail ต้องอยู่นอก try
- **Don't use try/catch for control flow**: ตรวจได้ → if/else ; ควบคุมไม่ได้ → try/catch
- **Don't re-throw silently**: log ก่อน re-throw เสมอ — เพิ่ม context ให้ error
- **Do check err.name**: แยกการจัดการตามประเภท error — ไม่ใช่ทุก error ควรถูกจัดการเหมือนกัน