การโหลด PDF ทุกครั้งพังด้วย ReferenceError ตัวเดียวกัน Stack trace ไม่ได้บอกอะไรที่มีประโยชน์เลย และข้อกำหนด (specification) ที่ใช้ไกด์การเขียนโค้ดก็ดูสมเหตุสมผลดีเมื่อดูบนกระดาษ มันบอกให้ตรวจสอบ feature flag และให้เปรียบเทียบแต่ละ region กับครึ่งหนึ่งของ pageWidth มันอธิบายว่าควรจะเกิดอะไรขึ้น แต่มันไม่ได้อธิบายว่า pageWidth ควรจะมาจากไหน และการละเลยเพียงจุดเดียวนี้ก็เพียงพอที่จะทำให้ pipeline ทั้งหมดพังลง
เอกสารสถาปัตยกรรม (Architecture documents) ทำหน้าที่ได้ดีในการอธิบายพฤติกรรม แต่มักจะทำได้แย่มากในการอธิบายขอบเขต (boundaries) ประโยคที่บอกว่า "ฟังก์ชันจะตรวจสอบ x เทียบกับ pageWidth" ไม่ใช่สัญญาทางเทคนิค (technical contract) แต่มันคือการบรรยายที่ซ่อน dependency ไว้ภายใต้ภาษาอังกฤษที่ดูเรียบง่าย เมื่อนักพัฒนาอ่านประโยคนั้นแล้วเขียนฟังก์ชันระดับโมดูล (module-level function) ที่อ้างถึง pageWidth โดยใช้ชื่อนั้น โค้ดจะดูถูกต้องเพราะมันตรงตามคำบรรยาย จากนั้นเมื่อ runtime พยายามจะหาชื่อนั้นแต่ไม่พบอะไรใน scope มันจึงโยน error ออกมา
การ Refactor ที่ควรจะง่าย
ฉันเห็นรูปแบบนี้เป๊ะๆ ระหว่างการ refactor การประกอบหน้า (page assembly) ข้อกำหนดระบุความต้องการที่ดูเหมือนจะสะอาดตาอยู่สองข้อ:
- ตรวจสอบ
FEATURE_LAYOUT - เปรียบเทียบแต่ละ region กับ
pageWidth / 2
นักพัฒนาทำตามคำแนะนำอย่างเคร่งครัด พวกเขาดึง utility function ออกมาไว้ที่ module scope และใส่ pageWidth ลงไปในฟังก์ชันโดยตรงโดยไม่ได้ระบุว่าเป็น parameter ข้อกำหนดไม่ได้ระบุว่า pageWidth ต้องส่งผ่าน argument list และไม่ได้ระบุว่าฟังก์ชันนั้นอยู่ที่ module scope ซึ่งเป็นจุดที่ pageWidth ไม่สามารถมองเห็นได้อีกต่อไป มันเพียงแค่ทึกทักเอาเองว่าผู้เขียนโค้ดจะเข้าใจบริบทการทำงาน (execution context) โดยนัย
ผลลัพธ์คือเกิด ReferenceError ทุกครั้งที่มีการโหลด PDF เนื่องจากตัวแปรไม่ได้อยู่ใน module scope ฟังก์ชันจึง throw error ทันที หากข้อกำหนดระบุขอบเขตของฟังก์ชันและ input ของมันอย่างชัดเจน นักพัฒนาก็จะส่ง pageWidth เข้าไป และบั๊กนี้ก็จะเป็นไปไม่ได้ในเชิงโครงสร้าง แต่กลายเป็นว่าคำแนะนำนั้นทำหน้าที่เหมือนกับกับดัก ที่ล่อลวงให้ผู้เขียนโค้ดพยายามเอื้อมมือออกไปหา parent scope ที่ไม่มีอยู่จริง
ความหมาย 4 นัยของคำว่า "ใช้" (Uses)
ปัญหาที่ลึกกว่านั้นคือ ภาษาเขียน (prose) ขาดระบบประเภทข้อมูล (type system) เมื่อข้อกำหนดบอกว่า "ฟังก์ชันใช้ X" ประโยคนี้มีความกำกวมอย่างน้อยสี่แง่มุมใน codebase ของ JavaScript สมัยใหม่:
- ฟังก์ชันรับ X เข้ามาเป็น formal parameter
- ฟังก์ชันอ่าน X จากตัวแปรระดับโมดูล (module-level variable) ที่ประกาศไว้ในไฟล์เดียวกัน
- ฟังก์ชันทำ closure เหนือ X จาก parent scope ที่ซ้อนอยู่
- ฟังก์ชันดึง X ออกมาจากออบเจกต์ที่ใหญ่กว่าซึ่งถูกส่งเข้ามา
ทุกตัวเลือกเหล่านี้ตรงตามคำบรรยายในข้อกำหนด และทุกตัวเลือกสามารถผ่านการทำ static analysis ได้ แต่มีเพียงตัวเลือกเดียวเท่านั้นที่ถูกต้องสำหรับขอบเขตที่กำหนด และการเลือกที่ผิดจะทำให้การทึกทักเอาเอง (assumptions) รั่วไหลข้ามขอบเขตนั้นไปในลักษณะที่คอมไพล์ผ่านอย่างเงียบเชียบ
โดยปกติแล้ว นักพัฒนาจะเลือกทางที่ง่ายที่สุดในขณะที่เขียน หาก pageWidth ดันอยู่ใน outer scope พวกเขาก็จะอ่านมันจากตรงนั้นแทนที่จะต้องไปแก้ไข function signature การทำ closure จะซ่อน dependency เอาไว้ โค้ดทำงานได้ในการรันครั้งแรก ผ่านชุดทดสอบ และถูกส่งขึ้นระบบ หลายสัปดาห์ต่อมา มีใครบางคนย้ายฟังก์ชันเดิมนั้นไปยังไฟล์อื่นเพื่อนำกลับมาใช้ใหม่ หรือเพื่อให้อ่านง่ายขึ้น Parent scope ก็หายไป โค้ดพัง และความพังนั้นดูเหมือนเป็น regression ใหม่ ทั้งที่สาเหตุที่แท้จริงคือ dependency ที่ถูกซ่อนไว้ตั้งแต่แรก
Web Workers ลบหลักฐานทิ้ง
ปัญหานี้จะกลายเป็นเรื่องร้ายแรงจริงๆ เมื่อมีการนำ Web Workers เข้ามาใช้ในสถาปัตยกรรม เมื่อเกิดข้อผิดพลาดภายใน worker เบราว์เซอร์จะลบข้อมูลที่คุณต้องการมากที่สุดทิ้งไป
นี่คือสิ่งที่เกิดขึ้นจริง ภายใน worker เมื่อเกิด exception ที่ไม่ได้รับการดักจับ (uncaught exception) มันจะส่ง ErrorEvent ออกมา หาก worker ส่งต่อ error นั้นไปยัง main thread รูปแบบปกติคือการดึงสตริง message แล้ว post ข้ามขอบเขตไป Main thread จะได้รับสตริงนั้น สร้างออบเจกต์ Error ใหม่ขึ้นมาจากมัน แล้วจึง log หรือ throw มันออกมา สิ่งที่ปรากฏใน DevTools คือ error ที่ถูกสร้างขึ้นใหม่ภายใน message handler ของ main thread ชื่อไฟล์เดิม, เลขบรรทัด และ stack trace จะถูกทิ้งไป ตำแหน่งที่แท้จริงของความล้มเหลวจะกลายเป็นสิ่งที่มองไม่เห็น
ดังนั้น เมื่อ pageWidth ที่หายไปทำให้เกิด ReferenceError ภายใน worker ตัว main thread จะรายงานเพียงข้อความ "pageWidth is not defined" ณ จุดที่มีการจัดการข้อความนั้น ส่วนฟังก์ชันระดับโมดูลที่แท้จริงนั้นอยู่ที่
