วิธีอ่านโค้ด Node.js ที่ไม่ได้เขียนเอง
ทุกโมดูลก่อนหน้านี้สอนให้คุณ เขียน Node.js ส่วนโมดูลนี้สอนอีกครึ่งหนึ่งของงาน นั่นคือการ อ่าน โค้ด Node.js ที่คนอื่น — หรือ AI — เขียนไว้ แล้วตัดสินว่ามันทำตามที่มันดูเหมือนจะทำจริงหรือไม่ AI assistant อาจส่งไฟล์ที่รันได้โดยไม่ throw หน้าตาเหมือนตัวอย่างที่ดีที่คุณเคยศึกษามาทุกอย่าง แต่ยังคงผิดอยู่ “รันแล้ว print ค่าถูกครั้งเดียว” ไม่เหมือนกับ “ถูกต้อง” — event loop, อายุของ stream, และขอบเขตข้อมูลที่ไม่มี type ของ Node ล้วนซ่อนรูปแบบความล้มเหลวที่จะโผล่มาก็ต่อเมื่อมี load สูง, connection ช้า, หรือ listener ตัวเดิมทำงานเป็นครั้งที่สอง นิสัยสี่ข้อด้านล่างคือลำดับที่ reviewer Node.js มืออาชีพใช้อ่านโค้ดจริง ๆ และทุกบทถัดไปในโมดูลนี้จะเจาะลึกทีละข้อ
ตาม event loop: อะไร block อะไร await
หัวข้อที่มีชื่อว่า “ตาม event loop: อะไร block อะไร await”ก่อนจะเชื่อ logic ของไฟล์ ให้หาก่อนว่าอะไรรันแบบ synchronous จริง ๆ และอะไรถูกเลื่อนออกไป
- อะไร block ทั้งโปรเซส?
readFileSync, loop synchronous ที่หนัก,JSON.parseบนสตริงขนาดใหญ่, เรียกcryptoแบบไม่ใช้ฟอร์ม async — ทั้งหมดนี้ยึด thread เดียวไว้ ทำให้ request, timer, และ callback อื่นทุกตัวต้องรอต่อคิว - อะไรถูก await จริง ๆ และอะไรแค่คืนค่า promise? การเรียก
async functionโดยไม่มีawaitนำหน้ายังคงรันอยู่ — เพียงแต่มันรันแบบขนานไปกับโค้ดถัดไป และผู้เรียกได้เดินหน้าต่อไปแล้วก่อนที่มันจะเสร็จ nextTick, microtask,setImmediate, และsetTimeoutรันตามลำดับไหนจริง ๆ? โค้ดที่ดูเรียงจากบนลงล่างบนหน้าจอ อาจสลับลำดับกันจริงในแบบที่เข้าใจได้ก็ต่อเมื่อรู้ว่า event loop กำหนดตารางเวลาของแต่ละคิวแยกกัน ไม่ได้อ่านตามตำแหน่งในไฟล์
ตามอายุของทุกทรัพยากร
หัวข้อที่มีชื่อว่า “ตามอายุของทุกทรัพยากร”ทุก stream, socket, และ listener มีจุดเริ่มต้น และ — ถ้าโค้ดถูกต้อง — มีจุดจบ การอ่านไฟล์ให้ดีคือการหาทั้งสองจุดของแต่ละตัว
- ทุก response ของ
http.createServerเรียกres.end()เพียงครั้งเดียวในทุก code path (รวมถึง path ที่เกิด error) หรือไม่? - ทุก stream ที่ถูกเปิด (
fs.createReadStream, socket, child process) ถูกปิดหรือ destroy ทั้งใน success path และ error path หรือแค่ path เดียว? - ทุก
.on('event', handler)มีการลบที่คู่กันหรือไม่ หรือมันสะสม listener ใหม่ทุกครั้งที่มี request เข้ามา — รูปแบบคลาสสิกของMaxListenersExceededWarning?
ตามขอบเขตของข้อมูล
หัวข้อที่มีชื่อว่า “ตามขอบเขตของข้อมูล”ขอบเขต (boundary) คือจุดใด ๆ ที่ข้อมูลเข้าสู่โปรแกรมจากที่ที่ระบบ type และภาษาไม่สามารถมองเห็นล่วงหน้าได้ เช่น process.env, body ของ HTTP request, process.argv, ไฟล์ที่อ่านจากดิสก์, ค่าจาก database driver การอ่านขอบเขตให้ดีคือการถามสำหรับแต่ละจุดว่า
- ค่านั้นถูกตรวจสอบจริง (type ถูกต้อง, รูปร่างถูกต้อง, ช่วงค่าถูกต้อง) ก่อนที่ส่วนอื่นของโปรแกรมจะเชื่อมันหรือไม่ หรือไหลตรงเข้าสู่ logic ที่สมมติว่ามันถูกต้องอยู่แล้ว?
process.env.PORTเป็นstringหรือundefinedเสมอ — โค้ดแปลงมันเป็นตัวเลขหรือยัง และจัดการกรณีที่ตัวแปรนี้ไม่มีอยู่เลยหรือไม่?- request body ถูกจำกัดขนาดก่อนถูก buffer หรือไม่ หรือการอัปโหลดที่ไม่จำกัดขนาดสามารถทำให้ memory โตไม่มีที่สิ้นสุด?
นับจำนวนจุดที่กลืน error
หัวข้อที่มีชื่อว่า “นับจำนวนจุดที่กลืน error”ก่อนตัดสินคุณภาพโดยรวมของไฟล์ ให้นับทุก catch {} ที่ว่างเปล่า, ทุก .catch(() => {}), และทุก process.on('uncaughtException', ...) หรือ process.on('unhandledRejection', ...) ที่แค่ log แล้วทำงานต่อแทนที่จะล้มเหลวให้เห็นชัด แต่ละจุดคือที่ที่ผู้เขียนตัดสินใจให้ error หายไปแทนที่จะแสดงออกมา — บางครั้งก็มีเหตุผลที่ปลอดภัยจริง (เช่นการเขียน cache แบบ best-effort) และบางครั้งก็เพราะ bug ถูกปิดเสียงไว้ระหว่างพัฒนาแล้วไม่เคยกลับมาแก้ ไฟล์ที่ไม่มีจุดกลืน error เลยและรันผ่าน node --test กับ --trace-warnings ได้สะอาด ถือว่าถูกทดสอบจริง ส่วนไฟล์ที่มี catch เงียบ ๆ ห้าจุด แทบไม่ได้พิสูจน์อะไรเลยว่าจะเกิดอะไรขึ้นเมื่อมีอะไรผิดพลาดจริง ๆ
การอ่านจากบนลงล่างทำให้ดูเหมือนลำดับคือ “ประมวลผล, ตรวจสต็อก, เก็บเงิน, เข้าคิว, เสร็จ” — ห้าขั้นตอนตามลำดับนั้น แต่ลำดับจริงคือ start processing 1, order queued 1, main done, payment charged for 1, inventory checked for 1: บรรทัด synchronous ทำงานเสร็จก่อนทั้งหมด จากนั้น microtask (Promise.resolve().then) รันก่อน macrotask (setTimeout) เสมอ ไม่ว่า delay จะเป็น 0 หรือไม่ก็ตาม ลำดับการอ่านกับลำดับการรันไม่ใช่สิ่งเดียวกัน และบท microtasks vs. macrotasks ในโมดูล Event Loop และ Async อธิบายเหตุผลไว้ครบ
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart TD A["Read the file"] --> B["Follow the event loop -- what blocks, what is awaited, what order do nextTick/microtask/setImmediate/setTimeout run in?"] B --> C["Follow every lifetime -- does every stream/socket/listener that opens also close, on every path?"] C --> D["Follow the boundaries -- env, request body, argv, disk, DB: validated, or just trusted?"] D --> E["Count the suppressions -- empty catch, swallowed rejection, silent uncaughtException handler"] E --> F["Only now: judge correctness"]
Reading order for Node.js you did not write:
1. Event loop -> what blocks the process, what is awaited, what actually runs first?2. Lifetimes -> every stream/socket/listener that opens: does it also close, on every path?3. Boundaries -> env / body / argv / disk / DB: validated, or just trusted?4. Suppressions -> how many catch blocks make an error disappear instead of surfacing?
Code that runs once without throwing answers none of these on its own.Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ได้อะไร | ประโยชน์ | ต้นทุน |
|---|---|---|
| อ่านลำดับ event loop ก่อน | จับ bug จากการสลับลำดับ (callback ที่ทำงานช้ากว่าที่โครงสร้างโค้ดบอก) ก่อนที่จะเชื่อ logic | ต้องจำกฎการจัดตารางเวลาไว้ในหัว ไม่ใช่แค่อ่านจากบนลงล่าง |
| ตามอายุของทรัพยากรอย่างชัดเจน | จับ listener ที่รั่วและ stream ที่ถูกปล่อยค้างไว้ใน error path ซึ่งจะโผล่มาก็ต่อเมื่อมี load ต่อเนื่อง | ช้ากว่าการสมมติว่า try/finally หรือ .on('close', ...) ที่เห็นผ่าน ๆ นั้นครบถ้วนแล้ว |
| ตามขอบเขตข้อมูล | จับข้อมูลที่ไม่น่าเชื่อถือก่อนที่มันจะไหลเข้าสู่ logic ที่สมมติว่ามันถูกต้องแล้ว | ต้องตรวจทุกจุดเข้า ไม่ใช่แค่จุดที่ดูน่าสงสัย |
| นับจำนวนจุดที่กลืน error | เปลี่ยน “รู้สึกว่าปลอดภัย” ให้เป็นตัวเลขที่เทียบกันระหว่างไฟล์หรือ PR ได้ | ตัวเลขดิบไม่มีการถ่วงน้ำหนักความรุนแรง — uncaughtException ที่ถูกปิดเสียงหนึ่งจุดมีน้ำหนักมากกว่า catch ว่างที่ป้องกันการ log สิบจุด |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “รันแล้วไม่ throw แปลว่าถูกต้อง” ทุก pattern ในแคตตาล็อกของโมดูลนี้รันได้โดยไม่ throw บน happy path — นั่นแหละคือสิ่งที่ทำให้แต่ละ pattern อันตราย การรันผ่านครั้งเดียวพิสูจน์แค่ว่า input ที่ใช้ทดสอบไม่ได้ไปกระตุ้น bug ไม่ได้พิสูจน์ว่าไม่มี bug
- “โค้ด async รันตามลำดับที่เขียนไว้”
async/awaitและ promise ทำให้โค้ด async อ่าน ดูเรียงลำดับ แต่ไม่ได้ทำให้ operation async ที่ไม่เกี่ยวข้องกัน (timer ข้าง ๆ promise, สอง call ที่ไม่ await) รันตามลำดับการอ่านนั้นจริง - “ถ้าคนเขียนก็ไม่ต้องตรวจสอบขนาดนี้ ต้องเป็นโค้ด AI เท่านั้น” ทุก pattern ในแคตตาล็อกของโมดูลนี้ก็ปรากฏในโค้ด Node.js ที่คนเขียนเช่นกัน — AI แค่ผลิตมันในปริมาณที่มากกว่ามากและดูมั่นใจกว่า พร้อม comment ที่ทำให้ทางลัดดูเหมือนตั้งใจ
- “try/catch ครอบทั้งฟังก์ชันแปลว่าทุก error path ถูกจัดการแล้ว”
catchที่แค่ log แล้วคืนค่า default ได้ “จัดการ” error ในแง่ที่โปรเซสไม่ crash แต่มันอาจซ่อน bug ที่ควรทำให้ request ล้มเหลวแทนที่จะคืนข้อมูลผิด ๆ อย่างเงียบ ๆ
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อ reviewer เปิด diff ที่ AI เขียน ลำดับของโมดูลนี้กลายเป็น checklist
- grep diff หา
readFileSync,forEach(async,.then(,setTimeout,process.env,catch {},.catch(() => {}),process.on('uncaughtException'— ทุกจุดที่เจอคือจุดที่ต้องหยุดดูให้ดี - สำหรับทุก stream, socket, หรือ listener ที่ diff เปิดขึ้น ตรวจว่ามี close/destroy/removal ที่คู่กันในทุก code path รวมถึง error path
- สำหรับทุกค่าที่อ่านจาก
process.env, request body,process.argv, หรือดิสก์ ตรวจว่าถูก validate ก่อนที่ logic จะเชื่อ type และรูปร่างของมัน - ถ้า test ของ diff ผ่านและ
node --trace-warningsไม่ print อะไรเลย ให้ถือว่านั่นคือ “ไม่มี warning เกิดขึ้นกับ input นี้” ไม่ใช่ “ไม่มี bug”
🐛 หา bug ในโค้ดนี้
async function loadConfig(path) {try { const raw = await fs.readFile(path, 'utf8'); return JSON.parse(raw);} catch { return {};}}เฉลย
catch {} ที่ว่างเปล่าจัดการทุกความล้มเหลวที่เป็นไปได้เหมือนกันหมด: ไฟล์ config หาย, permission ผิด, ไฟล์ config มีอยู่แต่มี JSON ผิดรูปแบบ, และ disk I/O error ทั้งหมดกลายเป็น {} อย่างเงียบ ๆ ผู้เรียกไม่สามารถแยกได้ว่า “ไม่มี config จริง ๆ ใช้ default ได้เลย” กับ “config มีอยู่แต่พัง และโปรแกรมกำลังจะรันด้วยค่าที่ผิดอย่างเงียบ ๆ” ทางแก้คือให้ default เฉพาะกรณีที่คาดไว้จริง ๆ (ไฟล์ไม่พบ) แล้วปล่อยให้ทุกอย่างอื่นแสดงออกมา: ตรวจ err.code === 'ENOENT' ก่อนคืน {} แล้ว rethrow (หรือ log ให้ชัด) สำหรับกรณีอื่น — รวมถึง JSON.parse ที่ล้มเหลว ซึ่งมักหมายความว่าต้องให้คนแก้ไฟล์ ไม่ใช่ได้ default ว่างเปล่าอย่างเงียบ ๆ