โหมดมืด
บทที่ 6 — Error Handling + Debugging: ทำให้ server ไม่ตายง่าย ๆ
← บทที่ 5: HTTP + Web Server | สารบัญ | บทที่ 7: Config, CLI + Lifecycle →
ใน Node error 1 ตัวที่จัดการไม่ดี อาจทำให้ server ทั้งตัวตาย (จำได้ไหม — thread เดียว บทที่ 0) บทนี้สอนวิธีจัดการ error ให้ถูก แล้วต่อด้วยวิธี debug แบบมือโปร (ไม่ใช่ console.log รัว ๆ):
- Error object + custom error +
err.code - try/catch ครอบคลุมแค่ไหน — sync, await, แต่ไม่ครอบ callback/promise ที่ลืม catch
- operational vs programmer error — error แบบไหนควร "ตอบ user" แบบไหนควร "ตาย"
uncaughtException/unhandledRejection— ตาข่ายชั้นสุดท้าย + นโยบายที่ถูก- error handling ใน Express (รวม async)
- debug ด้วย
--inspect+ breakpoint แทน console.log
1. Error object และ custom error
JavaScript throw อะไรก็ได้ แต่ ควร throw Error เสมอ เพราะมันมี message + stack (ร่องรอยว่าพังที่ไหน):
javascript
throw new Error("อะไรพังสักอย่าง");
// custom error — สร้าง class ของเราเพื่อแยกประเภท error (จับเฉพาะที่ต้องการได้)
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError"; // ให้ stack/log บอกชื่อชัด
this.field = field; // ข้อมูลเสริม
this.statusCode = 400; // เอาไว้ map เป็น HTTP status ทีหลัง
}
}
try {
throw new ValidationError("email ไม่ถูกต้อง", "email");
} catch (err) {
if (err instanceof ValidationError) {
console.log(`ฟิลด์ ${err.field}: ${err.message}`); // จับเฉพาะ ValidationError
} else {
throw err; // error อื่นโยนต่อ — อย่ากลืน error ที่ไม่รู้จัก
}
}💡 core API มักแนบ
err.code(string คงที่ เช่นENOENT,ECONNREFUSED) — เช็กerr.codeแม่นกว่าอ่านerr.message(ข้อความเปลี่ยน/แปลภาษาได้ แต่ code ไม่เปลี่ยน)
2. try/catch ครอบคลุมแค่ไหน — จุดที่มือใหม่พังบ่อยสุด
try/catch จับได้เฉพาะ error ที่เกิด แบบ synchronous และ error จาก await เท่านั้น มันจับ error ที่เกิดใน callback หรือ Promise ที่ไม่ได้ await ไม่ได้:
javascript
// ✅ จับได้: sync error
try {
JSON.parse("ไม่ใช่ json");
} catch (err) { /* จับได้ */ }
// ✅ จับได้: await ที่ reject
try {
await readFile("ไม่มีไฟล์.txt");
} catch (err) { /* จับได้ */ }
// ❌ จับไม่ได้: error ใน callback async — มันเกิด "หลัง" try block จบไปแล้ว
try {
setTimeout(() => {
throw new Error("พังใน callback"); // ⚠️ ตัวนี้หลุดออกไป crash โปรแกรม!
}, 100);
} catch (err) {
// ไม่มีทางมาถึงตรงนี้ — try block จบไปก่อน setTimeout ทำงานแล้ว
}
// ❌ จับไม่ได้: Promise ที่ลืม await/catch
try {
readFile("ไม่มีไฟล์.txt"); // ไม่มี await! → กลายเป็น unhandledRejection
} catch (err) {
// ไม่เข้า — เพราะไม่ได้ await Promise นี้
}⚠️ กฎเหล็ก: Promise ทุกตัวต้องถูก
await(ในฟังก์ชัน async ที่มี try/catch) หรือมี.catch()ต่อท้าย — Promise ที่ลืมจัดการเรียกว่า unhandled rejection (รายละเอียดในข้อ 4) และจะลอยไปถึง process
💡 Error.cause (Node 16.9+) — เวลา wrap error ด้วยข้อความใหม่ ให้แนบ error เดิมไว้ใน
causeเพื่อไม่ทำให้ stack เดิมหาย:javascripttry { await readFile("config.json", "utf8"); } catch (err) { throw new Error("โหลด config ไม่ได้", { cause: err }); // err เดิมยังอยู่ใน .cause }log จะเห็นเป็น chain: error ใหม่ →
[cause]: error เดิม— debug ง่ายกว่า throw ใหม่เฉย ๆ ที่ทำ stack หาย
3. Operational error vs Programmer error — ตัดสินใจว่า "ตอบ" หรือ "ตาย"
แยกให้ออกก่อนตัดสินใจจัดการ:
| ประเภท | คือ | ตัวอย่าง | ทำยังไง |
|---|---|---|---|
| Operational | error ที่ "คาดไว้แล้ว" ในระบบที่ทำงานปกติ | input ผิด, ไฟล์ไม่เจอ, DB timeout, 404 | จัดการ + ตอบ user (เช่น 400/404/503) — server ทำงานต่อ |
| Programmer | บั๊กในโค้ดเรา | อ่าน property ของ undefined, เรียก function ที่ไม่มี, typo | ปล่อยให้ crash แล้ว restart — เพราะ state อาจเสียหายแล้ว เชื่อถือไม่ได้ |
หลักคิดสำคัญ (ขัดสัญชาตญาณมือใหม่): อย่าพยายาม "จับทุก error แล้วรันต่อ" ถ้าจับ programmer error (บั๊ก) แล้วปล่อยให้โปรแกรมรันต่อ ระบบจะอยู่ใน state ที่เพี้ยนโดยไม่รู้ตัว อาจส่งข้อมูลผิดให้ user โดยที่ไม่มีใครสังเกตเห็น การ crash แล้ว restart สะอาดกว่า เพราะได้ state ใหม่ที่ไม่เสียหาย (process manager จะ restart ให้อัตโนมัติ — บทที่ 11)
4. ตาข่ายชั้นสุดท้าย: uncaughtException + unhandledRejection
เมื่อ error หลุดทุกชั้น มันจะมาถึง process — Node ยิง 2 event นี้:
javascript
// error sync ที่ไม่มีใครจับเลย
process.on("uncaughtException", (err) => {
console.error("💥 uncaughtException:", err);
// log ให้ครบ แล้ว "ตายอย่างมีศักดิ์ศรี" — อย่ารันต่อใน state ที่ไม่รู้
process.exit(1);
});
// Promise ที่ reject แต่ไม่มีใคร .catch / await
process.on("unhandledRejection", (reason) => {
console.error("💥 unhandledRejection:", reason);
// ในของจริงควรเรียก graceful shutdown (บทที่ 7) ให้ request ที่ค้างจบก่อน
// แล้วค่อย exit — ไม่งั้น user กลางคันจะเจอ ECONNRESET
process.exit(1);
});💡 Node 15+ เปลี่ยน default: unhandled rejection จะทำให้ process exit อัตโนมัติ (เหมือน uncaughtException) — ปรับได้ด้วย flag
--unhandled-rejections=strict|warn|throw|none:
throw(default ตั้งแต่ 15) — exit processwarn— log warning แต่รันต่อ (พฤติกรรมเก่า Node 14)strict— exit แม้จะมี handlernone— เงียบสนิท (อันตราย ไม่แนะนำ)
นโยบายที่ถูกต้อง (ตามคำแนะนำทางการของ Node):
- 2 handler นี้มีไว้ log + ปิดให้เรียบร้อย + exit — ไม่ใช่ มีไว้ "กลืน error แล้วรันต่อ"
- หลัง
uncaughtExceptionprocess อยู่ใน state ที่เชื่อไม่ได้ → ควร exit แล้วให้ process manager (pm2/Kubernetes — บทที่ 11) restart ตัวใหม่ที่สะอาด- pm2/Kubernetes (k8s) คือเครื่องมือที่คอย restart process/container ให้อัตโนมัติเมื่อ crash — รายละเอียดเต็มอยู่บทที่ 11
- ก่อน exit ถ้าเป็น server ควรเรียก graceful shutdown (บทที่ 7) ให้ request ที่ค้างจบก่อน (drain in-flight) ไม่งั้น user กลางคันจะเจอ connection reset
- มองมันเป็น "กล่องดำของเครื่องบิน" — มีไว้บันทึกเหตุการณ์ก่อนตก ไม่ใช่เครื่องมือซ่อมระหว่างบิน
⚠️ Pitfall: เห็นโค้ดที่
process.on("uncaughtException", () => {})แล้วไม่ทำอะไร (กลืน error เงียบ ๆ ให้ server ไม่ตาย) — อันตรายมาก เพราะบั๊กถูกซ่อน ระบบทำงานผิดโดยไม่มีใครรู้ จนข้อมูลเสียหาย
5. Error handling ใน Express
Express มี error middleware พิเศษ — สังเกตว่ามี 4 พารามิเตอร์ (err, req, res, next) Express รู้ว่าเป็น error handler จากจำนวน argument:
javascript
import express from "express";
const app = express();
app.use(express.json());
// custom error จากข้อ 1 (สมมุติว่าประกาศไว้ตามแบบ ValidationError ข้างบน — statusCode = 400)
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
this.statusCode = 400;
}
}
app.get("/users/:id", (req, res) => {
const id = Number(req.params.id);
if (Number.isNaN(id)) throw new ValidationError("id ต้องเป็นตัวเลข"); // sync throw → Express จับได้
res.json({ id });
});
// ⚠️ ที่จับได้ตรงนี้เพราะเป็น throw แบบ "sync" (ไม่มี async/await) — ถ้า handler เป็น async แล้ว throw/reject
// ข้างใน (เช่น Promise ที่ await แล้ว fail) พฤติกรรมจะต่างกัน ดูตัวอย่างถัดไปด้านล่าง
// ⭐ error middleware ต้องอยู่ "ท้ายสุด" และมี 4 args — จุดรวม error ทั้งแอป
app.use((err, req, res, next) => {
console.error(err); // log error จริงไว้ฝั่ง server (ใน production ใช้ pino — ดูข้อ 6)
const status = err.statusCode ?? 500;
// ⚠️ อย่าส่ง err.message ของ 5xx กลับ client — อาจรั่ว stack/DB error/path ภายใน
// 4xx (operational) ส่ง message ได้ เพราะเราตั้งใจให้ user เห็น
const body = status >= 500
? { error: "Internal Server Error" }
: { error: err.message };
res.status(status).json(body);
});
app.listen(3000);ปัญหา: Express (4.x) ไม่จับ error จาก async handler อัตโนมัติ
javascript
// ❌ Express 4: ถ้า async handler reject → ไม่เข้า error middleware → unhandledRejection!
app.get("/bad", async (req, res) => {
const data = await readFile("ไม่มีไฟล์"); // reject → หลุด!
res.json(data);
});
// ✅ วิธีแก้ที่ 1: ครอบ try/catch + next(err) เอง
// (ในไฟล์จริง import ต้องอยู่ "บนสุดของไฟล์" เสมอ ตามกฎ ESM static import จากบทที่ 1
// ที่เขียนไว้ตรงนี้เพื่อให้ตัวอย่างนี้อ่านจบในตัวเอง — ไม่ต้องเลื่อนขึ้นไปดูข้างบน)
import { readFile } from "node:fs/promises";
app.get("/ok", async (req, res, next) => {
try {
const data = await readFile("config.json", "utf8");
res.json(JSON.parse(data));
} catch (err) {
next(err); // ส่งเข้า error middleware
}
});💡 หมายเหตุเวอร์ชัน: Express 5 (เสถียรแล้วในปี 2025) จับ error จาก async handler ให้อัตโนมัติ ไม่ต้อง try/catch ทุก route แล้ว — ถ้าใช้ Express 4 อยู่ ครอบเองหรือใช้ helper เช่น
express-async-errorsหมายเหตุล่วงหน้า: NestJS ก็แก้ปัญหานี้ให้ตั้งแต่แรก (มี exception filter จับ async error อัตโนมัติ) — ถ้ายังไม่ถึงภาค Nest ข้ามได้ จะมีบทเฉพาะที่ NestJS บทที่ 3
6. Debugging แบบมือโปร — เลิก console.log รัว ๆ
console.log ใช้ได้ แต่มีขีดจำกัด: ต้องเดาว่าจะ log ตรงไหน, log เยอะจนรก, ดู state ทั้งหมด ณ จุดหนึ่งไม่ได้ debugger จริงให้คุณหยุดโปรแกรม (breakpoint) แล้วส่องตัวแปรทุกตัว
วิธีที่ 1: node --inspect + Chrome DevTools
bash
node --inspect app.js # เปิด debugger port (รันต่อทันที)
node --inspect-brk app.js # หยุดที่บรรทัดแรกทันที (debug ตอน startup)
node --inspect-wait app.js # (Node 18+) รัน แต่ "รอ" จน debugger ต่อค่อยเริ่มแล้วเปิด Chrome ไปที่ chrome://inspect → กด "inspect" → ได้ DevTools เต็มรูปแบบ (breakpoint, watch, call stack, console) เหมือน debug frontend
ขั้นตอนละเอียด (เผื่อทำครั้งแรก):
- รัน
node --inspect-brk app.jsใน terminal — จะเห็นข้อความDebugger listening on ws://127.0.0.1:9229/... - เปิด Chrome → พิมพ์
chrome://inspectใน address bar - ใต้ "Remote Target" จะเห็นไฟล์ของเรา → กด "inspect" → DevTools เปิดขึ้นมา
- โค้ดหยุดที่บรรทัดแรก → ตั้ง breakpoint ที่บรรทัดอื่นได้ (คลิกซ้ายของเลขบรรทัด) → กด ▶ Resume (F8) → โปรแกรมรันไปหยุดที่ breakpoint ถัดไป
- ส่องตัวแปรในแท็บ Scope, ใส่ expression ใน Watch, เดินทีละบรรทัดด้วย Step Over (F10) / Step Into (F11)
💡 รัน test ด้วย debugger ก็ใช้แนวเดียวกัน:
node --inspect-brk --test tests/แล้วต่อ DevTools — set breakpoint ใน test/source code ได้เหมือนกัน
วิธีที่ 2: VS Code debugger (แนะนำที่สุด)
VS Code มี debugger ในตัว — วิธีเร็วสุด:
- เปิดไฟล์ที่จะ debug
- คลิกซ้ายของเลขบรรทัด → จุดแดง (breakpoint) โผล่
- กด F5 → เลือก "Node.js" → โปรแกรมรันแล้วหยุดที่ breakpoint
- ส่องตัวแปร, กด step over (F10) / step into (F11) ทีละบรรทัด
หรือใส่คำว่า debugger; ในโค้ด — เมื่อรันด้วย debugger จะหยุดตรงนั้นเอง:
javascript
app.get("/users/:id", (req, res) => {
const id = Number(req.params.id);
debugger; // โปรแกรมหยุดตรงนี้ (ตอนรันใน debug mode) ให้ส่อง id, req
res.json({ id });
});console ก็มีของดีกว่า log
javascript
console.error("error ไป stderr"); // stderr = ช่องสำหรับ error (คู่กับ stdout = ช่อง output ปกติ) → production แยก error log ออกได้
console.table([{ id: 1 }, { id: 2 }]); // แสดง array ของ object เป็นตารางสวย
console.time("db"); /* ...งาน... */ console.timeEnd("db"); // จับเวลา → "db: 42ms"
console.dir(obj, { depth: null }); // ดู object ลึกทุกชั้น💡 ใน production อย่าใช้
console.logเป็น logging จริง — ใช้ structured logger เช่นpino(เร็วที่สุด, async-safe) หรือwinstonที่ log เป็น JSON มี level/timestamp พร้อม redact ฟิลด์ลับ:javascriptimport pino from "pino"; const log = pino({ level: process.env.LOG_LEVEL ?? "info" }); log.info({ userId: 42, requestId }, "user logged in"); log.error({ err }, "DB query failed"); // serialize stack ให้อัตโนมัติpino/winston รองรับการ redact (= ปิดบัง/ลบข้อมูลลับ เช่น password, token ก่อน log ออกไป) ฟิลด์ลับอัตโนมัติ กันไม่ให้ข้อมูลอ่อนไหวหลุดไปอยู่ใน log
ผูกกับ
AsyncLocalStorageเพื่อแนบrequestIdทุก log line ของ request เดียวกัน (correlation) — debug production ง่ายขึ้นมาก · รายละเอียดเต็มในบทที่ 11 ·console.logไว้ debug ตอน dev เท่านั้น
💡 อยาก trace ลึก ๆ ใช้
node:diagnostics_channel(built-in) ปล่อย event ของแอป + ฟังprocess.on("warning", ...)เพื่อจับ deprecation warning เร็วก่อนเครื่องจริงเจอ
🛠️ Checkpoint 6 — ลงมือก่อนไปบทถัดไป
- สร้าง
class NotFoundError extends Errorที่มีstatusCode = 404แล้ว throw + จับด้วยinstanceof - เขียนโค้ดที่พิสูจน์ว่า try/catch จับ error ใน
setTimeoutcallback ไม่ได้ — แล้วแก้ให้จับได้ (ใบ้: try/catch ต้องอยู่ "ข้างใน" callback) - ตั้ง handler
unhandledRejectionแล้วจงใจสร้าง Promise ที่ reject โดยไม่ await — ดูว่า handler ทำงาน - ใน Express ลองทำ async route ที่ throw (Express 4) แล้วดูว่ามันหลุดเป็น unhandledRejection จริงไหม → แก้ด้วย try/catch +
next(err)→ ใส่ error middleware ท้ายสุดให้ตอบ JSON สวย ๆ - รันโปรแกรมด้วย
node --inspect-brkแล้วต่อ Chrome DevTools (chrome://inspect) ตั้ง breakpoint แล้วส่องตัวแปร — หรือใช้ VS Code กด F5 - ใช้
console.time/timeEndวัดว่าฟังก์ชันหนึ่งใช้เวลาเท่าไหร่
เฉลยข้อ 2:
javascriptsetTimeout(() => { try { // try/catch ต้องอยู่ในตัว callback เอง throw new Error("พัง"); } catch (err) { console.error("จับได้แล้ว:", err.message); } }, 100);
สรุปบทที่ 6
throw new Errorเสมอ (มี stack) · custom error (extends Error) แยกประเภทได้ · เช็กerr.codeแม่นกว่า message- try/catch จับได้เฉพาะ sync +
await— ไม่จับ error ใน callback หรือ Promise ที่ลืม await - แยก operational error (input ผิด/ไฟล์ไม่เจอ → จัดการ+ตอบ user) จาก programmer error (บั๊ก → ปล่อย crash + restart)
uncaughtException/unhandledRejection= ตาข่ายสุดท้าย → log แล้ว exit อย่ากลืน error รันต่อ- Express: error middleware 4 args
(err, req, res, next)ท้ายสุด · Express 4 ต้องครอบ async เอง (next(err)), Express 5/NestJS จัดการให้ - debug ด้วย
--inspect+ DevTools / VS Code breakpoint /debugger;แทน console.log รัว ๆ
บทต่อไป: จัดการ config ผ่าน environment (12-factor), ทำ CLI tool, และ graceful shutdown — ปิด server โดยไม่ทิ้ง request ค้าง