การดีบัก Node.js
Node.js Inspector
หัวข้อที่มีชื่อว่า “Node.js Inspector”Node มี inspector ในตัวที่อิงจาก Chrome DevTools Protocol เริ่มใช้งานด้วย flag --inspect แล้วเชื่อมต่อด้วย debugger ที่รองรับ CDP ได้ทุกตัว — Chrome DevTools, VS Code, หรือ WebStorm
# Start with inspector (process continues running, connects on demand)node --inspect src/app.js
# Start AND pause on the very first line of your codenode --inspect-brk src/app.jsหลังจาก launch แล้ว เปิด chrome://inspect ใน Chrome คลิก “Open dedicated DevTools for Node” จากนั้นคุณจะได้ breakpoints แบบเต็มรูปแบบ, call stacks, การตรวจสอบ scope, heap snapshots, และ CPU profiling — เหมือนกับ panel สำหรับ browser debugging ทุกประการ
inspector ฟังที่ port 9229 เป็นค่าเริ่มต้นเสมอ (จะเป็น port อื่นก็ต่อเมื่อคุณส่ง --inspect=<port>) และมันพูดโปรโตคอลเดียวกับ Chrome DevTools Protocol ที่ browser DevTools ใช้ — WebSocket เข้ามา คำสั่งและ event เป็น JSON ไม่ว่า client ตัวไหน (Chrome, VS Code, WebStorm) จะเป็นตัวที่เชื่อมต่ออยู่ก็ตาม:
flowchart LR App["node --inspect app.js"] -->|"opens"| Port["ws://127.0.0.1:9229/uuid"] Port -->|"Chrome DevTools Protocol (JSON over WebSocket)"| Client["Any CDP client"] Client --> Chrome["chrome://inspect"] Client --> VSCode["VS Code debugger"] Client --> CLI["node inspect (built-in CLI client)"]
node inspect — ตัว client บรรทัดคำสั่งในตัว
หัวข้อที่มีชื่อว่า “node inspect — ตัว client บรรทัดคำสั่งในตัว”คุณไม่จำเป็นต้องมี browser หรือ editor เลย: node inspect <file> เริ่ม process เป้าหมาย พร้อมกับ debugger REPL ใน terminal เดียวกัน โดยใช้โปรโตคอลเดียวกัน:
node inspect src/app.js< Debugger listening on ws://127.0.0.1:9229/<uuid>< For help, see: https://nodejs.org/en/docs/inspectorconnecting to 127.0.0.1:9229 ... okdebug> Break on start in src/app.js:1> 1 console.log('line1'); 2 debugger; 3 console.log('line2');debug> contพิมพ์ cont (หรือ c) เพื่อรันไปยัง breakpoint/debugger statement ถัดไป next/n เพื่อ step over step/s เพื่อ step into out/o เพื่อ step out watch('expr') เพื่อเพิ่ม watch expression และ repl เพื่อเข้า REPL ที่ scope อยู่ใน stack frame ปัจจุบัน มันช้ากว่า GUI ในการนำทาง แต่ไม่ต้องมีอะไรนอกจาก terminal — มีประโยชน์เมื่อทำงานผ่าน SSH หรือใน container ที่ไม่ได้ forward port
การเชื่อมต่อกับ VS Code
หัวข้อที่มีชื่อว่า “การเชื่อมต่อกับ VS Code”VS Code ตรวจจับ Node inspector อัตโนมัติ เพิ่ม .vscode/launch.json:
{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Debug App", "program": "${workspaceFolder}/src/app.js", "runtimeArgs": ["--inspect-brk"] }, { "type": "node", "request": "attach", "name": "Attach to running node", "port": 9229 } ]}กด F5 เพื่อ launch หรือ Ctrl+Shift+P → “Attach to Node Process” เพื่อเชื่อมต่อกับ --inspect server ที่กำลังทำงานอยู่
คำสั่ง debugger
หัวข้อที่มีชื่อว่า “คำสั่ง debugger”ใส่ debugger; ที่ไหนก็ได้ใน JavaScript เมื่อ inspector เชื่อมต่ออยู่ การทำงานจะหยุด (pause) ที่บรรทัดนั้น — เหมือนการตั้ง breakpoint ในโค้ด:
function calculateTotal(items) { let total = 0; for (const item of items) { debugger; // execution pauses here when inspector is open total += item.price * item.qty; } return total;}เมื่อไม่มี inspector เชื่อมต่ออยู่ debugger จะถูก ignore อย่างเงียบ ๆ ลบหรือปิดใช้งาน debugger ก่อน deploy ขึ้น production เสมอ
เมธอด console ที่มีมากกว่า console.log
หัวข้อที่มีชื่อว่า “เมธอด console ที่มีมากกว่า console.log”object console มีเมธอดมากกว่าที่นักพัฒนาส่วนใหญ่รู้จัก:
// Tabular output — great for arrays of objectsconst users = [ { name: 'Alice', age: 30, role: 'admin' }, { name: 'Bob', age: 25, role: 'user' },];console.table(users);// Prints a formatted ASCII table with columns: name, age, role
// Deep inspection — prints non-enumerable properties and prototype chainconst map = new Map([['key', 'value']]);console.dir(map, { depth: 3, colors: true });
// Timing a code blockconsole.time('database-query');// ... run expensive operation ...console.timeEnd('database-query');// Prints: database-query: 42.183ms
// Count invocationsfunction hit() { console.count('hit');}hit(); // hit: 1hit(); // hit: 2hit(); // hit: 3
// Group related outputconsole.group('User details');console.log('name: Alice');console.log('role: admin');console.groupEnd();
// Conditional breakpoint-style assertionconsole.assert(1 === 2, 'Math is broken'); // prints assertion errorconsole.assert(1 === 1, 'This never prints');console.trace() พิมพ์ call stack ปัจจุบันโดยไม่หยุดการทำงาน — มีประโยชน์เวลาตอบคำถาม “ใครเรียกตัวนี้” โดยไม่ต้องแนบ debugger เลย:
Needs the Node.js runtime — open in StackBlitz to run.
demo นี้ไม่มีแผง verified-output ด้วยเหตุผลเดียวกับ live demo ก่อนหน้า: stack trace มี file path และเลขบรรทัดที่มีความหมาย (และเหมือนกัน) เฉพาะบนเครื่องที่รันมันเท่านั้น — ไม่ใช่สิ่งที่ควรตรึงลงเป็น snapshot “verified” ตัวเดียว
ตรวจสอบ object ที่ซ้อนลึกและ stack trace ที่ยาว
หัวข้อที่มีชื่อว่า “ตรวจสอบ object ที่ซ้อนลึกและ stack trace ที่ยาว”formatter ค่าเริ่มต้นของ console.log (ที่ใช้ util.inspect อยู่ข้างใต้) จะหยุดลงลึกใน object ที่ซ้อนกันหลัง depth 2 — ระดับที่ลึกกว่านั้นจะถูกย่อเป็น [Object] ส่ง depth เองเมื่อต้องการมากกว่านั้น:
Output (verified on Node 24)
{ a: { b: { c: [Object] } } }
{ a: { b: [Object] } }
{
a: { b: { c: { d: 1 } } }
}--stack-trace-limit กำหนดว่า Error.prototype.stack จะเก็บกี่ frame (ค่าเริ่มต้นของ V8 คือ 10) ลดค่านี้ในสคริปต์ที่พิมพ์ stack ยาวมากจากการเรียกแบบ recursive ลึก ๆ หรือเพิ่มค่าเมื่อต้องการ context มากกว่า 10 frame:
node --stack-trace-limit=2 -e "function a(){b()} function b(){c()} function c(){throw new Error('x')} try{a()}catch(e){console.log(e.stack.split('\n').length)}"3เมื่อจำกัดที่ 2 e.stack มีทั้งหมด 3 บรรทัด: header Error: x บวกกับ stack frame อีก 2 อัน — c และ b — a ถูกตัดออกไป
ตาม trace warning และ throw ที่ไม่ถูกจับกลับไปยังต้นตอ
หัวข้อที่มีชื่อว่า “ตาม trace warning และ throw ที่ไม่ถูกจับกลับไปยังต้นตอ”flag สองตัวเปลี่ยน warning หรือข้อความ crash บรรทัดเดียวให้กลายเป็น stack trace เต็มที่ชี้ไปยังจุดที่มันถูกสร้างหรือ throw จริง ๆ:
node --trace-warnings depwarn.mjslegacy(node:12345) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead. at showFlaggedDeprecation (node:buffer:195:11) at new Buffer (node:buffer:278:3) at file:///path/to/depwarn.mjs:1:11ถ้าไม่มี --trace-warnings Node จะพิมพ์แค่ warning บรรทัดเดียวกับคำแนะนำให้รันซ้ำด้วย flag — ซึ่งเป็นบรรทัดที่คุณจะต้องเดาสาเหตุเองถ้าไม่มีมัน --trace-uncaught ทำสิ่งเดียวกันสำหรับ exception โดยเฉพาะเมื่อจุดที่ throw ต่างจากจุดที่ object Error ถูกสร้างขึ้นจริง (เช่น error ที่สร้างไว้ก่อนแล้วค่อย throw ทีหลังจาก callback ของ setTimeout):
node --trace-uncaught trace-uncaught-demo.mjsfile:///path/to/trace-uncaught-demo.mjs:6 throw err; ^
Error: created early, thrown later at file:///path/to/trace-uncaught-demo.mjs:2:13Thrown at: at file:///path/to/trace-uncaught-demo.mjs:2:13
Node.js v24.13.0ส่วน Thrown at: พิเศษนี้ปรากฏเฉพาะเมื่อมี flag เท่านั้น — ปกติคุณจะได้ stack จากจุดที่ Error ถูกสร้างเท่านั้น ซึ่งอาจเป็นคนละบรรทัดกับจุดที่มันถูก throw จริง ๆ
NODE_DEBUG — การ trace โมดูล built-in
หัวข้อที่มีชื่อว่า “NODE_DEBUG — การ trace โมดูล built-in”core module หลายตัว (http, net, timers, module, cluster, tls) จะพิมพ์บรรทัด diagnostic ภายในเมื่อคุณตั้งค่า NODE_DEBUG เป็นชื่อของมัน:
NODE_DEBUG=http node server-and-client.mjsHTTP 12345: createConnection localhost:54321:HTTP 12345: sockets localhost:54321: 1 1HTTP 12345: outgoing message end.(node:12345) Warning: Setting the NODE_DEBUG environment variable to 'http' can expose sensitive data (such as passwords, tokens and authentication headers) in the resulting log.HTTP 12345: SERVER new http connectionNode เตือนเรื่องนี้ไว้เอง: NODE_DEBUG=http log internal ของ request/response ซึ่งในแอปจริงอาจมี authentication header และข้อมูลอ่อนไหวอื่น ๆ ปนอยู่ ใช้มันในเครื่องเพื่อเข้าใจการใช้ connection ซ้ำและพฤติกรรม keep-alive แต่อย่าเปิดใช้กับ traffic ที่มี credential จริงเด็ดขาด
uncaughtException และ unhandledRejection — พฤติกรรมค่าเริ่มต้นที่แท้จริง
หัวข้อที่มีชื่อว่า “uncaughtException และ unhandledRejection — พฤติกรรมค่าเริ่มต้นที่แท้จริง”ทั้งคู่ทำให้ process crash เป็นค่าเริ่มต้น — Node ถือว่า exception ที่ไม่มีใครจับ หรือ rejection ที่ไม่มีใครจัดการ หมายความว่า state ของโปรแกรมไม่สามารถเชื่อถือได้อีกต่อไป:
// @expect-throw uncaught sync exception crashes by defaultthrow new Error('sync boom');// @expect-throw unhandled rejection crashes by default (has since Node 15)Promise.reject(new Error('boom'));คุณ listen ได้ทั้งสองแบบ — แต่การ listen ไม่ได้แปลว่าปลอดภัยที่จะทำงานต่อ มันแปลว่า คุณ เป็นคนตัดสินใจว่าอะไรคือ “ปลอดภัย”:
Output (verified on Node 24)
before reject
after scheduling rejection
unhandled rejection: async boomการตั้ง process.exitCode (แทนที่จะเรียก process.exit() จากใน handler) ทำให้ I/O ที่ค้างอยู่หรือ cleanup ทำงานเสร็จได้ พร้อมกับยังมั่นใจว่า process จะ exit แบบ non-zero ในที่สุด เพื่อให้ CI, process manager, หรือ on-call engineer สังเกตเห็นว่ามีอะไรผิดพลาด
ทำไม “แค่เพิ่ม handler แล้วทำงานต่อ” ถึงอันตราย
หัวข้อที่มีชื่อว่า “ทำไม “แค่เพิ่ม handler แล้วทำงานต่อ” ถึงอันตราย”Output (verified on Node 24)
tick 1
swallowed: corrupt state at tick 2 -- process kept running, state may now be inconsistent
tick 3callback ของ interval ที่ throw ตอน counter === 2 ไม่มีโอกาสได้รัน logic ของตัวเองต่อจาก throw เลย — ไม่ว่ามันควรจะทำอะไรใน tick นั้น (อัปเดต counter, ปล่อย lock, เขียนไฟล์ให้เสร็จ) ก็ไม่เกิดขึ้น แต่ process ก็ยังทำงานต่อราวกับไม่มีอะไรเกิดขึ้น เอกสารของ Node.js พูดชัดเจนเรื่องนี้: หลังจาก uncaught exception “การใช้งานที่ถูกต้อง” ของ event นี้คือทำ cleanup แบบ synchronous แล้วปล่อยให้ process exit — ไม่ใช่กลับไปทำงานปกติต่อ handler ของ uncaughtException มีไว้สำหรับ log และ shutdown อย่างสง่างาม ไม่ใช่ try/catch แบบ global
ขั้นตอนการดีบักที่ใช้งานจริง
หัวข้อที่มีชื่อว่า “ขั้นตอนการดีบักที่ใช้งานจริง”# 1. Add --inspect-brk to pause before any code runsnode --inspect-brk src/server.js
# 2. Open chrome://inspect → click your target
# 3. Set breakpoints in the Sources panel, or hit the existing debugger statement
# 4. Step through: F10 (step over), F11 (step into), F8 (resume)
# 5. Inspect variables in the Scope panel or hover over them
# 6. Use the Console panel to run expressions in the current scopeการดีบักกับ nodemon (watch + inspect)
หัวข้อที่มีชื่อว่า “การดีบักกับ nodemon (watch + inspect)”# Auto-restart AND keep inspector open on changesnpx nodemon --inspect src/app.js
# With ts-node for TypeScriptnpx nodemon --exec "node --inspect --loader ts-node/esm" src/app.tsแบบจำลองความคิด (Mental Model)
หัวข้อที่มีชื่อว่า “แบบจำลองความคิด (Mental Model)”เครื่องมือดีบักทุกตัวในหน้านี้ตอบคำถามเดียวกัน — “จริง ๆ แล้วตอนนี้เกิดอะไรขึ้นใน process นี้” — ที่ระดับความล่วงล้ำต่างกัน: เมธอด console.* ราคาถูก เปิดอยู่ตลอด ไม่เปลี่ยนแปลงการทำงานเลย --trace-warnings/--trace-uncaught/NODE_DEBUG เพิ่มรายละเอียดให้ output ที่จะเกิดขึ้นอยู่แล้ว --inspect/--inspect-brk หยุดจริงและให้คุณ step ผ่านได้ ส่วน handler ของ uncaughtException/unhandledRejection คือแนวป้องกันสุดท้ายเมื่อมีอะไรผิดพลาดไปแล้ว ให้ใช้เครื่องมือที่ถูกที่สุดที่ตอบคำถามของคุณได้ก่อนเสมอ
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ต้นทุน |
|---|---|---|
console.log/console.trace | ไม่ต้องตั้งค่าอะไร ใช้ได้ทุกที่รวมถึง log ใน production | ไม่มี breakpoint ไม่มีการตรวจตัวแปรแบบ live ลืมทิ้งไว้ในโค้ดง่าย |
node --inspect + DevTools/VS Code | breakpoint เต็มรูปแบบ, step through, ตรวจ scope แบบ live, heap snapshot | ต้องมี GUI client และสำหรับ process ที่รันบน remote/container ต้อง forward port |
node inspect (CLI) | ทำงานผ่าน SSH ได้ ไม่ต้อง forward port นอกจาก shell เอง | นำทางช้ากว่า ไม่มี tree ตัวแปรแบบ visual |
จัดการ uncaughtException/unhandledRejection | มีโอกาส log context และ shutdown อย่างสะอาดแทนที่จะ crash แบบทันที | ถ้าจัดการผิด (กลับไปทำงานปกติต่อ) จะซ่อน bug จริงและเสี่ยง state เสียหาย — ทางที่ปลอดภัยยังคงเป็นการ exit |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “unhandled promise rejection แค่พิมพ์ warning ไม่ทำให้ process crash” — ความจริง: ตั้งแต่ Node 15 unhandled rejection ทำให้ process terminate เป็นค่าเริ่มต้น เหมือนกับ uncaught synchronous exception ทุกประการ
- “การเพิ่ม
process.on('uncaughtException', ...)handler ทำให้ปลอดภัยที่จะทำงานต่อ” — ความจริง: callback ที่ throw หยุดทำงานที่จุดthrowเลย — อะไรก็ตามที่มันควรจะทำต่อไม่เกิดขึ้น การใช้งาน handler นี้ที่ถูกต้องตามเอกสารคือ cleanup แบบ synchronous แล้ว exit ไม่ใช่กลับไปทำงานปกติต่อ - “console.log แสดง object ทั้งหมดไม่จำกัดความลึก” — ความจริง:
util.inspect(ที่console.logใช้อยู่ข้างใน) หยุดที่ depth 2 เป็นค่าเริ่มต้น ระดับที่ลึกกว่านั้นจะพิมพ์เป็น[Object]เว้นแต่คุณจะส่ง{ depth: null }หรือตัวเลขที่สูงกว่า - “—trace-warnings กับ —trace-uncaught ทำสิ่งเดียวกัน” — ความจริง:
--trace-warningsเพิ่ม stack ให้ process warning (deprecation ฯลฯ) ส่วน--trace-uncaughtเพิ่ม stack ที่สอง — จุดที่ exception ถูก throw จริง ๆ — สำหรับ uncaught exception ที่จุด throw ต่างจากจุดที่Errorถูกสร้าง
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”process.on('uncaughtException', ...)หรือprocess.on('unhandledRejection', ...)handler แค่ log แล้วทำงานต่อ โดยไม่มีprocess.exit()/process.exitCodeและไม่ปิด resource ที่เปิดอยู่ (server, connection ฐานข้อมูล, timer) หรือไม่? นั่นคือ anti-pattern “กลืนแล้วทำงานต่อ” พอดี — ค่าเริ่มต้นที่ปลอดภัยคือ cleanup แล้ว exit- โค้ดดีบักทิ้ง
console.trace()หรือ log แบบNODE_DEBUGไว้ทำงานอยู่บน path ที่จัดการข้อมูลผู้ใช้จริงหรือไม่? stack trace และ debug log อาจรั่วไหล internal หรือ credential เข้าไปใน production log ได้ - มี
debugger;statement อยู่ในโค้ดที่กำลังจะ commit หรือ deploy หรือไม่? มันไม่มีผลถ้าไม่มี inspector เชื่อมต่ออยู่ แต่ก็เป็นสัญญาณว่าโค้ดไม่ได้ถูกทำความสะอาดหลัง debug เสร็จ - โค้ด error-handling สมมติว่า
err.stackมี context เต็มเสมอ โดยไม่คำนึงถึง--stack-trace-limitหรือความเป็นไปได้ที่Errorถูกสร้างไกลจากจุดที่ถูก throw (สิ่งที่--trace-uncaughtมีไว้เพื่อเปิดเผยพอดี) หรือไม่?
🐛 หา bug ในโค้ดนี้
import http from 'node:http';
const server = http.createServer((req, res) => {res.end('ok');});
process.on('uncaughtException', (err) => {console.error('caught an error, continuing:', err.message);});
server.listen(3000);เฉลย
handler นี้ log error แล้ว return — ไม่มี process.exit() ไม่มี server.close() ไม่มีอะไรที่หยุด process เลย request handler ที่กำลังรันตอนที่ exception ถูก throw ไม่มีทางทำงานจบ (response อาจไม่ถูกส่งเลย และ client ที่เชื่อมต่ออยู่ก็แค่ค้างจนกว่าจะ timeout ของตัวเอง) ทุก request ในอนาคตจะถูกให้บริการโดย process ที่ internal state ไม่รู้ว่าเป็นอย่างไรแล้ว: transaction ฐานข้อมูลถูกทิ้งไว้แบบเปิดอยู่หรือเปล่า? lock ไม่เคยถูกปล่อยหรือเปล่า? counter ถูกอัปเดตค้างไว้ครึ่งเดียวหรือเปล่า? เอกสารของ Node.js พูดชัดเจนว่าการใช้งานที่ถูกต้องของ uncaughtException คือ cleanup แบบ synchronous แล้วปล่อยให้ process exit — ไม่ใช่กลืน error แล้วให้บริการ traffic ต่อ ทางแก้: log พร้อม context เต็ม ปิด server เพื่อหยุดรับ connection ใหม่ ทำงานที่ค้างอยู่ให้เสร็จหรือยกเลิกมัน แล้วเรียก process.exit(1) (หรือตั้ง process.exitCode = 1 แล้วปล่อยให้งานที่ค้างอยู่ drain) เพื่อให้ orchestrator restart process เข้าสู่สถานะที่รู้ว่าดี