हर PDF लोड होने पर एक ही ReferenceError के साथ क्रैश हो रहा था। स्टैक ट्रेस (stack trace) किसी काम की जानकारी नहीं दे रहा था, और वह स्पेसिफिकेशन (specification) जिसने कोड का मार्गदर्शन किया था, कागज़ पर बिल्कुल तर्कसंगत लग रही थी। इसमें कहा गया था कि एक फीचर फ्लैग (feature flag) की जाँच करें, और प्रत्येक क्षेत्र (region) की तुलना pageWidth के आधे से करें। इसने बताया कि क्या होना चाहिए। लेकिन यह बताने में विफल रहा कि pageWidth कहाँ से आना चाहिए था, और उस एक कमी ने पूरे पाइपलाइन को ठप करने के लिए पर्याप्त काम किया।

आर्किटेक्चर दस्तावेज़ व्यवहार समझाने में अच्छे होते हैं। वे अक्सर सीमाओं (boundaries) को समझाने में बहुत खराब होते हैं। एक वाक्य जो कहता है "फंक्शन pageWidth के विरुद्ध x की जाँच करता है" वह कोई तकनीकी अनुबंध (technical contract) नहीं है; यह एक वर्णन है जो साधारण अंग्रेजी के भीतर एक डिपेंडेंसी (dependency) को छिपा देता है। जब एक डेवलपर उस वाक्य को पढ़ता है और एक मॉड्यूल-लेवल फंक्शन लिखता है जो नाम से pageWidth को संदर्भित करता है, तो कोड सही दिखता है क्योंकि यह विवरण को पूरा करता है। फिर रनटाइम उस नाम को हल करने की कोशिश करता है, स्कोप (scope) में कुछ नहीं पाता है, और एरर थ्रो कर देता है।

वह रिफैक्टर (Refactor) जो सरल होना चाहिए था

मैंने पेज असेंबली रिफैक्टर के दौरान ठीक यही पैटर्न देखा था। स्पेसिफिकेशन में दो स्पष्ट रूप से साफ आवश्यकताएं सूचीबद्ध थीं:

  • FEATURE_LAYOUT की जाँच करें।
  • प्रत्येक क्षेत्र की तुलना pageWidth / 2 से करें।

डेवलपर ने निर्देशों का पूरी निष्ठा से पालन किया। उन्होंने मॉड्यूल स्कोप में एक यूटिलिटी फंक्शन निकाला और pageWidth को बिना पैरामीटर के रूप में घोषित किए सीधे फंक्शन बॉडी में डाल दिया। स्पेसिफिकेशन में यह नहीं कहा गया था कि pageWidth को आर्गुमेंट लिस्ट के माध्यम से आना चाहिए। इसमें यह भी नहीं कहा गया था कि फंक्शन मॉड्यूल स्कोप में स्थित है, जहाँ pageWidth अब दिखाई नहीं दे रहा था। इसने बस यह मान लिया कि लागू करने वाले (implementer) को निष्पादन संदर्भ (execution context) की अंतर्निहित समझ है।

इसका परिणाम हर PDF लोड पर ReferenceError के रूप में निकला। क्योंकि वेरिएबल मॉड्यूल स्कोप से अनुपस्थित था, फंक्शन तुरंत एरर थ्रो कर गया। यदि स्पेसिफिकेशन ने स्पष्ट रूप से फंक्शन की सीमा और उसके इनपुट्स का नाम लिया होता, तो डेवलपर pageWidth को पास कर देता, और यह बग संरचनात्मक रूप से असंभव होता। इसके बजाय, निर्देश एक जाल की तरह काम कर गया, जिसने लागू करने वाले को एक ऐसे पैरेंट स्कोप तक पहुँचने के लिए आमंत्रित किया जो अस्तित्व में ही नहीं था।

"Uses" के चार अर्थ

गहरा मुद्दा यह है कि गद्य (prose) में टाइप सिस्टम की कमी होती है। जब कोई स्पेसिफिकेशन कहता है "फंक्शन X का उपयोग करता है," तो एक आधुनिक JavaScript कोडबेस में यह वाक्य कम से कम चार विशिष्ट तरीकों से संदिग्ध होता है:

  • फंक्शन X को एक फॉर्मल पैरामीटर के रूप में प्राप्त करता है।
  • फंक्शन उसी फ़ाइल में घोषित मॉड्यूल-लेवल वेरिएबल से X को पढ़ता है।
  • फंक्शन एक नेस्टेड पैरेंट स्कोप से X को क्लोज (close over) करता है।
  • फंक्शन अपने अंदर पास किए गए एक बड़े ऑब्जेक्ट से X को निकालता है।

इनमें से प्रत्येक विकल्प स्पेसिफिकेशन की शब्दावली को संतुष्ट करता है। इनमें से प्रत्येक स्टैटिक एनालिसिस (static analysis) पास कर लेता है। लेकिन एक दी गई सीमा के लिए केवल एक ही सही है, और गलत चुनाव उस सीमा के पार धारणाओं को इस तरह लीक कर देता है कि वह बिना किसी चेतावनी के कंपाइल हो जाता है।

डेवलपर आमतौर पर लिखते समय सबसे आसान रास्ता चुनते हैं। यदि pageWidth किसी बाहरी स्कोप में मौजूद है, तो वे फंक्शन सिग्नेचर को बदलने के बजाय उसे वहीं से पढ़ लेंगे। एक क्लोजर (closure) डिपेंडेंसी को छिपा देता है। कोड पहली बार में काम करता है, टेस्ट सूट पास करता है, और शिप हो जाता है। हफ्तों बाद, कोई पुन: उपयोग के लिए या पठनीयता में सुधार के लिए उसी फंक्शन को किसी अलग फ़ाइल में ले जाता है। पैरेंट स्कोप गायब हो जाता है। कोड टूट जाता है, और यह टूट-फूट एक नए रिग्रेशन (regression) की तरह दिखती है, भले ही मूल कारण मूल छिपी हुई डिपेंडेंसी ही थी।

Web Workers सबूत मिटा देते हैं

यह समस्या तब वास्तव में गंभीर हो जाती है जब आर्किटेक्चर में Web Workers शामिल होते हैं। जब वर्कर के अंदर कोई त्रुटि होती है, तो ब्राउज़र उस जानकारी को हटा देता है जिसकी आपको सबसे अधिक आवश्यकता होती है।

यहाँ वास्तव में क्या होता है: एक वर्कर के अंदर, एक अनकॉट एक्सेप्शन (uncaught exception) एक ErrorEvent फायर करता है। यदि वर्कर उस एरर को मेन थ्रेड (main thread) पर भेजता है, तो सामान्य पैटर्न message स्ट्रिंग को पकड़ना और उसे सीमा के पार पोस्ट करना होता है। मेन थ्रेड उस स्ट्रिंग को प्राप्त करता है, उससे एक नया Error ऑब्जेक्ट बनाता है, और उसे लॉग या री-थ्रो करता है। DevTools में जो दिखाई देता है वह मेन थ्रेड के मैसेज हैंडलर के अंदर पुनर्गठित एरर है। मूल फ़ाइल नाम, लाइन नंबर और स्टैक ट्रेस को हटा दिया जाता है। विफलता का वास्तविक स्थान अदृश्य हो जाता है।

इसलिए जब गायब pageWidth ने वर्कर के अंदर ReferenceError ट्रिगर किया, तो मेन थ्रेड ने केवल "pageWidth is not defined" टेक्स्ट उस स्थान पर रिपोर्ट किया जहाँ मैसेज को हैंडल किया गया था। वास्तविक मॉड्यूल-लेवल फंक्शन में स्थित था