प्रत्येक PDF लोड होताना तोच ReferenceError येत होता. स्टॅक ट्रेस (stack trace) कशातही उपयुक्त माहिती दाखवत नव्हता, आणि कोडसाठी वापरलेली स्पेसिफिकेशन (specification) कागदावर अगदी योग्य वाटत होती. त्यात एक फीचर फ्लॅग (feature flag) तपासण्यास आणि प्रत्येक रिजनची (region) तुलना pageWidth च्या अर्ध्याशी करण्यास सांगितले होते. त्यात काय घडायला हवे याचे वर्णन होते. पण pageWidth कुठून यायला हवे, याचे वर्णन त्यात नव्हते आणि त्या एका त्रुटीमुळे संपूर्ण पाइपलाइन (pipeline) कोलमडून पडली.
आर्किटेक्चर डॉक्युमेंट्स (Architecture documents) वर्तन (behavior) स्पष्ट करण्यात चांगले असतात. परंतु, सीमा (boundaries) स्पष्ट करण्यात ते अनेकदा अपयशी ठरतात. "फंक्शन pageWidth च्या विरुद्ध x तपासते" असे म्हणणारे वाक्य हा तांत्रिक करार (technical contract) नसतो; ते साध्या इंग्रजीमध्ये लपवलेले एक वर्णन (narrative) असते ज्यामध्ये एखादी डिपेंडन्सी (dependency) दडलेली असते. जेव्हा एखादा डेव्हलपर ते वाक्य वाचतो आणि pageWidth चा संदर्भ देणारे मॉड्यूल-लेव्हल फंक्शन (module-level function) लिहितो, तेव्हा तो कोड योग्य वाटतो कारण तो वर्णनाचे पालन करतो. त्यानंतर रनटाइम (runtime) त्या नावाचा शोध घेण्याचा प्रयत्न करतो, स्कोपमध्ये (scope) काहीही सापडत नाही आणि एरर (error) थ्रो करतो.
जे रिफॅक्टरिंग (Refactor) सोपे असायला हवे होते
पेज असेंब्ली रिफॅक्टरिंग दरम्यान मी हेच नेमके पॅटर्न पाहिले. स्पेसिफिकेशनमध्ये दोन स्पष्ट गरजा (requirements) दिल्या होत्या:
FEATURE_LAYOUTतपासा.- प्रत्येक रिजनची तुलना
pageWidth / 2शी करा.
डेव्हलपरने सूचनांचे तंतोतंत पालन केले. त्यांनी मॉड्यूल स्कोपमध्ये (module scope) एक युटिलिटी फंक्शन (utility function) काढले आणि pageWidth ला पॅरामीटर म्हणून न सांगता थेट फंक्शन बॉडीमध्ये टाकले. स्पेसिफिकेशनमध्ये असे म्हटलेले नव्हते की pageWidth आर्ग्युमेंट लिस्टद्वारे (argument list) आले पाहिजे. फंक्शन मॉड्यूल स्कोपमध्ये आहे आणि तिथे pageWidth दृश्यमान (visible) नाही, असेही त्यात नमूद नव्हते. त्यामध्ये फक्त असे गृहीत धरले होते की अंमलबजावणी करणाऱ्याला (implementer) एक्झिक्यूशन कॉन्टेक्स्ट (execution context) आपोआप समजेल.
परिणामी प्रत्येक PDF लोडवर ReferenceError येत होता. व्हेरिएबल मॉड्यूल स्कोपमध्ये नसल्यामुळे फंक्शन लगेच एरर थ्रो करत होते. जर स्पेसिफिकेशनमध्ये फंक्शनची सीमा (boundary) आणि त्याचे इनपुट्स (inputs) स्पष्टपणे नमूद केले असते, तर डेव्हलपरने pageWidth पास केले असते आणि ही त्रुटी स्ट्रक्चरलदृष्ट्या अशक्य झाली असती. त्याऐवजी, त्या सूचना एखाद्या सापळ्यासारख्या (trap) ठरल्या, ज्याने अंमलबजावणी करणाऱ्याला अस्तित्वात नसलेल्या पेरेंट स्कोपमध्ये (parent scope) शोध घेण्यास प्रवृत्त केले.
"Uses" चे चार अर्थ
मूळ समस्या अशी आहे की गद्य लेखनात (prose) टाइप सिस्टम (type system) नसते. जेव्हा एखादे स्पेसिफिकेशन म्हणते की "फंक्शन X वापरते (uses X)," तेव्हा आधुनिक JavaScript कोडबेसमध्ये हे वाक्य किमान चार प्रकारे संदिग्ध (ambiguous) असू शकते:
- फंक्शन X ला फॉर्मल पॅरामीटर (formal parameter) म्हणून प्राप्त करते.
- फंक्शन त्याच फाईलमध्ये घोषित केलेल्या मॉड्यूल-लेव्हल व्हेरिएबलमधून (module-level variable) X वाचते.
- फंक्शन नेस्टेड पेरेंट स्कोपमधून (nested parent scope) X क्लोज (close) करते.
- फंक्शन त्याला पास केलेल्या मोठ्या ऑब्जेक्टमधून (object) X काढते.
यापैकी प्रत्येक पर्याय स्पेसिफिकेशनच्या शब्दांशी सुसंगत आहे. यापैकी प्रत्येक पर्याय स्टॅटिक अनालिसिस (static analysis) मध्ये पास होतो. परंतु दिलेल्या मर्यादेसाठी (boundary) त्यापैकी फक्त एकच पर्याय योग्य असतो, आणि चुकीचा पर्याय त्या मर्यादेच्या पलीकडे असे गृहीत धरतो जे कंपाईल होताना शांतपणे (silently) सुटते.
डेव्हलपर्स सहसा लिहिताना सर्वात सोपा मार्ग निवडतात. जर pageWidth एखाद्या बाहेरील स्कोपमध्ये (outer scope) असेल, तर ते फंक्शन सिग्नेचर (function signature) बदलण्याऐवजी तिथूनच ते वाचतील. क्लोजर (closure) मुळे ती डिपेंडन्सी लपली जाते. कोड पहिल्या रनमध्ये काम करतो, टेस्ट सूट पास होतो आणि शिप होतो. काही आठवड्यांनंतर, कोणीतरी पुन्हा वापरण्यासाठी किंवा वाचनीयता सुधारण्यासाठी तेच फंक्शन दुसऱ्या फाईलमध्ये हलवते. पेरेंट स्कोप नाहीसा होतो. कोड तुटतो आणि ही त्रुटी एखाद्या नवीन रिग्रेशन (regression) सारखी वाटते, जरी मूळ कारण ती मूळ लपलेली डिपेंडन्सीच असली तरीही.
वेब वर्कर्स (Web Workers) पुरावे पुसून टाकतात
एकदा आर्किटेक्चरमध्ये वेब वर्कर्स (Web Workers) आले की ही समस्या अधिक गंभीर होते. जेव्हा वर्करमध्ये एरर येतो, तेव्हा ब्राउझर तुम्हाला हवी असलेली महत्त्वाची माहिती काढून टाकतो.
येथे नेमके काय घडते ते पहा. वर्करच्या आत, एखादा अनकॉट एक्सेप्शन (uncaught exception) ErrorEvent फायर करतो. जर वर्करने तो एरर मेन थ्रेडला (main thread) पाठवला, तर सामान्य पद्धत म्हणजे message स्ट्रिंग घेणे आणि ती मर्यादेच्या पलीकडे पोस्ट करणे. मेन थ्रेड ती स्ट्रिंग प्राप्त करतो, त्यापासून एक नवीन Error ऑब्जेक्ट तयार करतो आणि तो लॉग करतो किंवा पुन्हा थ्रो करतो. DevTools मध्ये जे दिसते ते मेन थ्रेडच्या मेसेज हँडलरमधील (message handler) पुन्हा तयार केलेला एरर असतो. मूळ फाईलचे नाव, लाईन नंबर आणि स्टॅक ट्रेस (stack trace) काढून टाकले जातात. त्रुटीचे नेमके स्थान अदृश्य होते.
त्यामुळे जेव्हा वर्करमध्ये pageWidth च्या अभावामुळे ReferenceError ट्रिगर झाला, तेव्हा मेन थ्रेडने मेसेज हँडल केलेल्या ठिकाणी फक्त "pageWidth is not defined" हा मजकूर रिपोर्ट केला. प्रत्यक्ष मॉड्यूल-लेव्हल फंक्शन...
