প্রতিটি PDF লোড একই ReferenceError দিয়ে ক্র্যাশ করছিল। স্ট্যাক ট্রেসটি কোনো কাজের দিকে নির্দেশ করছিল না, আর কোডটি নির্দেশ করার জন্য যে স্পেসিফিকেশনটি ব্যবহার করা হয়েছিল, সেটি কাগজে-কলমে একদম যুক্তিসঙ্গত মনে হচ্ছিল। সেখানে বলা ছিল একটি ফিচার ফ্ল্যাগ চেক করতে এবং প্রতিটি রিজিয়নকে pageWidth-এর অর্ধেক দিয়ে তুলনা করতে। এটি বর্ণনা করেছিল কী হওয়া উচিত। কিন্তু এটি বর্ণনা করতে ব্যর্থ হয়েছিল যে pageWidth কোথা থেকে আসা উচিত ছিল, আর এই একটিমাত্র বাদ পড়া তথ্যই পুরো পাইপলাইনটি অচল করে দেওয়ার জন্য যথেষ্ট ছিল।
আর্কিটেকচার ডকুমেন্টগুলো আচরণ ব্যাখ্যা করতে দক্ষ। কিন্তু সীমানা (boundaries) ব্যাখ্যা করার ক্ষেত্রে সেগুলো প্রায়ই খুব খারাপ। একটি বাক্য যা বলে "ফাংশনটি pageWidth-এর বিপরীতে x চেক করে", সেটি কোনো টেকনিক্যাল কন্ট্রাক্ট নয়; এটি একটি বর্ণনা যা সাধারণ ইংরেজি শব্দের আড়ালে একটি ডিপেন্ডেন্সি লুকিয়ে রাখে। যখন একজন ডেভেলপার সেই বাক্যটি পড়েন এবং একটি মডিউল-লেভেল ফাংশন লেখেন যা নাম দিয়ে pageWidth-কে রেফার করে, তখন কোডটি সঠিক মনে হয় কারণ এটি বর্ণনাটি পূরণ করে। তারপর রানটাইম যখন নামটি সমাধান (resolve) করার চেষ্টা করে, তখন স্কোপের মধ্যে কিছুই খুঁজে পায় না এবং এরর থ্রো করে।
যে রিফ্যাক্টরটি সহজ হওয়া উচিত ছিল
একটি পেজ অ্যাসেম্বলি রিফ্যাক্টরের সময় আমি ঠিক এই প্যাটার্নটি দেখেছিলাম। স্পেসিফিকেশনটি দুটি আপাতদৃষ্টিতে পরিষ্কার প্রয়োজনীয়তা তালিকাভুক্ত করেছিল:
FEATURE_LAYOUTচেক করুন।- প্রতিটি রিজিয়নকে
pageWidth / 2-এর সাথে তুলনা করুন।
ডেভেলপারটি অনুগতভাবে নির্দেশাবলী অনুসরণ করেছিলেন। তারা মডিউল স্কোপে একটি ইউটিলিটি ফাংশন তৈরি করেছিলেন এবং pageWidth-কে প্যারামিটার হিসেবে উল্লেখ না করেই সরাসরি ফাংশন বডিতে বসিয়ে দিয়েছিলেন। স্পেসিফিকেশনে এটি উল্লেখ করা হয়নি যে pageWidth অবশ্যই একটি আর্গুমেন্ট লিস্টের মাধ্যমে আসতে হবে। এটি এটাও বলেনি যে ফাংশনটি মডিউল স্কোপে অবস্থিত, যেখানে pageWidth আর দৃশ্যমান নয়। এটি কেবল ধরে নিয়েছিল যে ইমপ্লিমেন্টার বা বাস্তবায়নকারী পরোক্ষভাবে এক্সিকিউশন কনটেক্সটটি বুঝে নিয়েছেন।
এর ফলে প্রতিটি PDF লোডেই একটি ReferenceError দেখা দিচ্ছিল। যেহেতু ভেরিয়েবলটি মডিউল স্কোপে ছিল না, ফাংশনটি সাথে সাথে এরর থ্রো করছিল। স্পেসিফিকেশনটি যদি স্পষ্টভাবে ফাংশনের সীমানা এবং এর ইনপুটগুলোর নাম উল্লেখ করত, তবে ডেভেলপার pageWidth পাস করে দিতেন এবং এই বাগটি গঠনগতভাবে অসম্ভব হতো। পরিবর্তে, নির্দেশনাটি একটি ফাঁদের মতো কাজ করেছিল, যা ইমপ্লিমেন্টারকে এমন একটি প্যারেন্ট স্কোপে হাত বাড়াতে প্ররোচিত করেছিল যা আসলে বিদ্যমান ছিল না।
"Uses"-এর চারটি অর্থ
মূল সমস্যাটি হলো গদ্যের (prose) কোনো টাইপ সিস্টেম নেই। যখন একটি স্পেসিফিকেশন বলে "the function uses X," তখন একটি আধুনিক JavaScript কোডবেসে বাক্যটি অন্তত চারটি উপায়ে অস্পষ্ট হতে পারে:
- ফাংশনটি X-কে একটি ফরমাল প্যারামিটার হিসেবে গ্রহণ করে।
- ফাংশনটি একই ফাইলে ঘোষিত একটি মডিউল-লেভেল ভেরিয়েবল থেকে X পড়ে।
- ফাংশনটি একটি নেস্টেড প্যারেন্ট স্কোপ থেকে X-কে ক্লোজ (close over) করে।
- ফাংশনটি তার মধ্যে পাস করা একটি বড় অবজেক্ট থেকে X-কে এক্সট্র্যাক্ট করে।
এই প্রতিটি অপশনই স্পেসিফিকেশনের শব্দচয়ন অনুযায়ী সঠিক। প্রতিটিই স্ট্যাটিক অ্যানালাইসিসে পাস করে। কিন্তু একটি নির্দিষ্ট সীমানার জন্য কেবল একটিই সঠিক, আর ভুল পছন্দটি সেই সীমানা জুড়ে এমনভাবে অনুমানগুলোকে ছড়িয়ে দেয় যা নিঃশব্দে কম্পাইল হয়ে যায়।
ডেভেলপাররা সাধারণত লেখার সময় সবচেয়ে সহজ পথটি বেছে নেন। যদি pageWidth কোনো বাইরের স্কোপে থাকে, তবে তারা ফাংশনের সিগনেচার পরিবর্তন না করে সেখান থেকেই এটি রিড করবেন। একটি ক্লোজার (closure) ডিপেন্ডেন্সি বা নির্ভরশীলতাকে লুকিয়ে ফেলে। কোডটি প্রথমবার চলে, টেস্ট স্যুট পাস করে এবং শিপ করা হয়। কয়েক সপ্তাহ পরে, কেউ হয়তো পুনরায় ব্যবহারের জন্য বা রিডিবিলিটি উন্নত করার জন্য সেই একই ফাংশনটি অন্য একটি ফাইলে সরিয়ে নেয়। প্যারেন্ট স্কোপটি হারিয়ে যায়। কোডটি ভেঙে পড়ে, এবং এই ভাঙনটি একটি নতুন রিগ্রেশন হিসেবে মনে হয়, যদিও এর মূল কারণ ছিল সেই আদি লুকিয়ে থাকা ডিপেন্ডেন্সি।
Web Workers প্রমাণ মুছে ফেলে দেয়
আর্কিটেকচারে Web Workers যুক্ত হওয়ার সাথে সাথে এই সমস্যাটি আরও মারাত্মক হয়ে ওঠে। যখন কোনো ওয়ার্কারের ভেতরে কোনো এরর ঘটে, ব্রাউজার আপনার সবচেয়ে প্রয়োজনীয় তথ্যগুলো সরিয়ে ফেলে দেয়।
আসলে যা ঘটে তা হলো: একটি ওয়ার্কারের ভেতরে একটি আনক্যাচড এক্সেপশন (uncaught exception) একটি ErrorEvent ট্রিগার করে। যদি ওয়ার্কারটি সেই এররটি মেইন থ্রেডে পাঠায়, তবে সাধারণ নিয়ম হলো message স্ট্রিংটি নিয়ে সীমানা অতিক্রম করে পাঠানো। মেইন থ্রেড সেই স্ট্রিংটি গ্রহণ করে, তা থেকে একটি নতুন Error অবজেক্ট তৈরি করে এবং সেটি লগ করে বা পুনরায় থ্রো করে। DevTools-এ যা দেখা যায় তা হলো মেইন থ্রেডের মেসেজ হ্যান্ডলারের ভেতরে পুনর্গঠিত সেই এররটি। আসল ফাইলের নাম, লাইন নম্বর এবং স্ট্যাক ট্রেস বাদ পড়ে যায়। ত্রুটির প্রকৃত অবস্থান অদৃশ্য হয়ে যায়।
তাই যখন অনুপস্থিত pageWidth ওয়ার্কারের ভেতরে একটি ReferenceError ট্রিগার করেছিল, মেইন থ্রেড কেবল "pageWidth is not defined" টেক্সটটি রিপোর্ট করেছিল যেখানে মেসেজটি হ্যান্ডেল করা হয়েছিল। আসল মডিউল-লেভেল ফাংশনটি ছিল
