كل عملية تحميل لملفات PDF كانت تنهار مع نفس الخطأ ReferenceError. لم يشير تتبع المكدس (stack trace) إلى أي شيء مفيد، والمواصفات التي وجهت الكود بدت منطقية تماماً على الورق؛ فقد نصت على التحقق من علامة ميزة (feature flag)، ومقارنة كل منطقة بنصف قيمة pageWidth. لقد وصفت المواصفات ما يجب أن يحدث، لكنها فشلت في وصف المصدر الذي من المفترض أن تأتي منه pageWidth ، وكانت تلك الفجوة الوحيدة كافية لإسقاط خط الإنتاج (pipeline) بأكمله.

وثائق الهندسة المعمارية (Architecture documents) جيدة في شرح السلوك، لكنها غالباً ما تكون سيئة جداً في شرح الحدود (boundaries). إن جملة تقول "تتحقق الدالة من x مقابل pageWidth" ليست عقداً تقنياً؛ بل هي سرد يخفي تبعية (dependency) داخل لغة إنجليزية بسيطة. عندما يقرأ المطور تلك الجملة ويكتب دالة على مستوى الوحدة (module-level function) تشير إلى pageWidth بالاسم، يبدو الكود صحيحاً لأنه يستوفي الوصف. ثم يحاول وقت التشغيل (runtime) تحديد الاسم، فلا يجد شيئاً في النطاق (scope)، فيقوم بإطلاق الخطأ.

عملية إعادة الهيكلة (Refactor) التي كان ينبغي أن تكون بسيطة

رأيت هذا النمط بالضبط أثناء عملية إعادة هيكلة لتجميع الصفحات. حيث أدرجت المواصفات متطلبين يبدوان واضحين:

  • التحقق من FEATURE_LAYOUT.
  • مقارنة كل منطقة مقابل pageWidth / 2.

اتبع المطور التعليمات بأمانة؛ حيث استخرج دالة مساعدة (utility function) على مستوى الوحدة ووضع pageWidth مباشرة داخل جسم الدالة دون تحديدها كمعامل (parameter). لم تنص المواصفات على وجوب وصول pageWidth عبر قائمة المعاملات (argument list)، ولم تنص على أن الدالة تعيش على مستوى الوحدة، حيث لم تعد pageWidth مرئية. لقد افترضت ببساطة أن المنفذ يفهم سياق التنفيذ (execution context) ضمنياً.

كانت النتيجة خطأ ReferenceError عند كل عملية تحميل لملف PDF. ولأن المتغير كان غائباً عن نطاق الوحدة (module scope)، فقد أطلقت الدالة خطأً فوراً. لو كانت المواصفات قد حددت صراحةً حدود الدالة ومدخلاتها، لقام المطور بتمرير pageWidth إليها، ولأصبح الخطأ مستحيلاً من الناحية الهيكلية. بدلاً من ذلك، تصرفت التعليمات وكأنها فخ، حيث دعت المنفذ للوصول إلى نطاق أب (parent scope) غير موجود.

المعاني الأربعة لكلمة "يستخدم" (Uses)

المشكلة الأعمق هي أن النثر يفتقر إلى نظام أنواع (type system). عندما تقول المواصفات "الدالة تستخدم X"، فإن الجملة تكون غامضة بأربع طرق محددة على الأقل في بيئة عمل JavaScript حديثة:

  • تستقبل الدالة X كمعامل رسمي (formal parameter).
  • تقرأ الدالة X من متغير على مستوى الوحدة (module-level variable) مُعلن في نفس الملف.
  • تقوم الدالة بعمل closure على X من نطاق أب متداخل.
  • تستخرج الدالة X من كائن أكبر يتم تمريره إليها.

كل خيار من هذه الخيارات يستوفي صياغة المواصفات، وكل واحد منها يجتاز التحليل الساكن (static analysis). ولكن واحد فقط منها هو الصحيح لحدود معينة، والاختيار الخاطئ يسرب افتراضات عبر تلك الحدود بطريقة يتم تجميعها (compiles) بصمت.

يختار المطورون عادةً المسار الأقل مقاومة في لحظة الكتابة. فإذا صادف وجود pageWidth في نطاق خارجي، فسيقرؤونها من هناك بدلاً من تعديل توقيع الدالة (function signature). يقوم الـ closure بإخفاء التبعية، فيعمل الكود في التشغيل الأول، ويجتاز مجموعة الاختبارات، ويتم إطلاقه. وبعد أسابيع، يقوم شخص ما بنقل نفس الدالة إلى ملف مختلف لإعادة استخدامها، أو لتحسين قابلية القراءة، فيختفي النطاق الأب. ينكسر الكود، ويبدو العطل وكأنه تراجع (regression) جديد، رغم أن السبب الجذري كان التبعية المخفية الأصلية.

عمال الويب (Web Workers) يمحون الأدلة

تصبح هذه المشكلة خبيثة حقاً بمجرد دخول عمال الويب (Web Workers) في الهندسة المعمارية. فعندما يحدث خطأ داخل العامل، يقوم المتصفح بتجريد المعلومات التي تحتاجها بشدة.

إليك ما يحدث فعلياً: داخل العامل، يؤدي استثناء غير ملتقط (uncaught exception) إلى إطلاق ErrorEvent. إذا قام العامل بتمرير هذا الخطأ إلى الخيط الرئيسي (main thread)، فإن النمط المعتاد هو التقاط سلسلة message وإرسالها عبر الحدود. يستقبل الخيط الرئيسي تلك السلسلة، ويبني منها كائن Error جديداً، ثم يسجله أو يعيد إلقاءه (re-throws). وما يظهر في DevTools هو الخطأ المعاد بناؤه داخل معالج الرسائل الخاص بالخيط الرئيسي. يتم التخلص من اسم الملف الأصلي، ورقم السطر، وتتبع المكدس (stack trace). ويصبح الموقع الحقيقي للفشل غير مرئي.

لذا، عندما تسبب pageWidth المفقود في إطلاق ReferenceError داخل العامل، أبلغ الخيط الرئيسي فقط عن النص "pageWidth is not defined" في الموقع الذي تمت فيه معالجة الرسالة. كانت الدالة الفعلية على مستوى الوحدة موجودة في