ஒவ்வொரு பிழைத்திருத்தத் திறனையும் சவால் விடும் வகையில் ஒரு பிழை அறிக்கை வந்தது. குறைந்தத் திறன் கொண்ட Android போன்களைப் பயன்படுத்தும் பயனர்கள், ஆப் (app) திடீரென மறைந்துவிடுவதாகக் கூறினர். அது தொடங்கும் போதோ அல்லது ஏதேனும் ஒரு குறிப்பிட்ட தொடுதல் (tap) அல்லது ஸ்வைப் (swipe) செய்யும் போதோ நடக்கவில்லை. பயன்பாட்டைத் தொடங்கி சுமார் இருபது நிமிடங்களில், திரை உறைந்து (freeze) செயல்முறை நின்றுவிடுகிறது. லாக் (logs) கோப்புகள் எவ்வித பிழையையும் காட்டவில்லை. QA குழுவினர் தங்களின் உயர்ரக வன்பொருள்களில் (high-end hardware) இதைச் செய்து பார்க்க முயன்றும், அதை மீண்டும் உருவாக்க முடியவில்லை. பின்பற்ற வேண்டிய எந்தக் குறிப்பிட்ட வழிமுறைகளும் இல்லை. மூன்று மணிநேர மெமரி ப்ரொஃபைலிங் (memory profiling) செய்த பிறகுதான் உண்மை தெரியவந்தது. ஒரு React hook-க்குள் ஒரு single event listener இருந்தது. அந்த listener ஒரு பெரிய தரவுத் தொகுப்பை (dataset) தன்னுடன் இணைத்துக் கொண்டிருந்தது (closed over). அந்த component unmount ஆனது, ஆனால் listener அப்படியே இருந்தது. தரவுத் தொகுப்பும் நினைவகத்தில் (memory) அப்படியே இருந்தது. 2GB RAM கொண்ட ஒரு சாதனத்தில், இந்தத் தரவுத் திரட்சி heap-ஐத் தீர்த்துவிட்டதால், இயங்குதளம் (OS) அந்த ஆப்பை நிறுத்திவிட்டது. இது ஒரு தொடரியல் பிழை (syntax error) அல்லது தர்க்கப் பிழை (logic flaw) அல்ல. இது ஒரு scope பிழை, மேலும் இது மிகத் தீவிரமானது.

ஒரு Closure எவ்வாறு Leak ஆகிறது

பெரும்பாலான பயிற்சிகள் (tutorials) scope என்பதை ஒரு மாறி (variable) எங்குத் தெரியும் என்பது குறித்த ஒரு கல்விசார் புதிராகவே கற்பிக்கின்றன. ஆனால் நடைமுறையில் (production), scope என்பது நினைவக ஆயுட்காலம் (memory lifetime) குறித்த ஒரு ஒப்பந்தமாகும். ஒரு JavaScript function ஒரு மாறியை closure மூலம் இணைத்துக் கொள்ளும்போது, அந்த closure இருக்கும் வரை அந்த மாறியை engine உயிர்ப்புடன் வைத்திருக்கும். ஒரு React component-இல், பயனர் அந்தப் பக்கத்திலிருந்து வெளியேறிய பிறகும், UI node மறைந்த பிறகும் உங்கள் தரவு நீடித்திருக்கும் என்று இது பொருள்படும்.

window object-இல் ஒரு listener-ஐப் பதிவு செய்யும் ஒரு hook-ஐக் கருத்தில் கொள்ளுங்கள். அந்த component render ஆகி, listener-ஐ இணைத்து, பின்னர் unmount ஆகிறது. ஒருவேளை cleanup கட்டம் விடுபட்டாலோ அல்லது தவறாக இருந்தாலோ, அந்த listener அப்படியே இருக்கும். ஒவ்வொரு முறை புதிய mount நடக்கும்போதும், சிக்கியிருக்கும் தரவின் மற்றொரு நகல் RAM-இல் சேர்கிறது. போதுமான நினைவகம் கொண்ட ஒரு டெவலப்பர் வொர்க்ஸ்டேஷனில் (developer workstation), இந்த அதிகரிப்பை நீங்கள் கவனிக்காமல் போகலாம். ஆனால் Android Go இயங்கும் பட்ஜெட் போனில், இருபது நிமிட சாதாரணப் பயன்பாடு கூட கிடைக்கும் heap-ஐத் தீர்த்துவிடப் போதுமானது. இயங்குதளம் (OS) தலையிட்டு அந்தச் செயல்முறையைத் (process) துண்டிக்கிறது. பதிவு செய்ய எந்தத் தவறும் (exception) இருக்காது. சிஸ்டம் நேரடியாக இணைப்பைத் துண்டித்துவிடும்.

இதனால்தான் scope என்பது நினைவக மேலாண்மை (memory management) என்று அழைக்கப்படுகிறது. Lexical environment என்பது வெறும் தத்துவார்த்த எல்லை அல்ல. அது ஒரு retention graph ஆகும். நீங்கள் சேகரிக்கப்படாத (uncollected) ஒரு closure-க்குள் விட்டுச் செல்லும் ஒவ்வொரு மாறியும், இறுதியில் உங்கள் ஆப்பைச் சுற்றியே ஒரு சுவரை எழுப்பும் செங்கற்களாக மாறும்.

Production Apps-களை Scope அழிக்கும் மூன்று வழிகள்

Scope சிக்கல்கள் அனைத்தும் ஒரே மாதிரியாக இருக்காது. சில மெதுவாக நினைவகத்தை உறிஞ்சும். மற்றவை உடனடியாகப் பாதிப்பை ஏற்படுத்தும். ஆப்-களைப் பாதிக்கும் முறையான வடிவங்கள் (patterns) இதோ:

Global Scope Pollution

Micro-frontend கட்டமைப்புகள் குழுக்கள் சுதந்திரமாகச் செயல்பட அனுமதிக்கின்றன, ஆனால் அவை அனைத்தும் ஒரே window object-ஐப் பகிர்ந்து கொள்கின்றன. ஒரு பயன்பாடு window.config போன்ற ஒரு global மாறியை அமைக்கும்போதோ அல்லது window-இல் ஒரு பொதுவான utility-ஐ இணைக்கும்போதோ, அது தனித்து இயங்குவதில்லை. மற்றொரு குழுவின் ஆப் அதே global மாறிக்கு வேறு ஒரு வடிவத்தை எதிர்பார்க்கலாம், அல்லது அதன் தொடக்கத்தின் போது (bootstrap) அதை மாற்றியமைக்கலாம். இதன் விளைவாக, நிறுவனத்தின் வளர்ச்சியோடு சேர்ந்து ஒரு feature collision ஏற்படும். ஒரு repository-யில் உள்ள டெவலப்பருக்கு, தான் செய்யும் ஒரு குறுக்குவழி (shortcut) மற்றொரு குழுவின் செயல்பாட்டைப் பாதிக்கும் என்பது தெரியாது. பயன்பாட்டுப் பரப்பு (surface area) வளர வளர, இந்த globals அனைத்தும் பகிரப்பட்ட நிலத்தில் புதைக்கப்பட்ட கண்ணிவெடிகளைப் போல மாறும்.

Closure Memory Leaks

Single page applications பல மணிநேரங்கள் இயங்கும் வகையில் உருவாக்கப்படுகின்றன. அந்தத் தொடர்ச்சியான செயல்பாட்டினால் தான், утеறும் (leaking) closures நச்சுத்தன்மை வாய்ந்ததாக மாறுகின்றன. இந்த முறை மிகவும் பொதுவானது: ஒரு useEffect, ஒரு global event bus, WebSocket handler அல்லது DOM-இல் ஒரு callback-ஐப் பதிவு செய்கிறது. dependency array நிலையற்றதாகவோ அல்லது விடுபட்டோ இருந்தால், cleanup என்பது அசல் சந்தாவோடு (subscription) பொருந்தாது. அந்த closure அதன் lexical scope-இல் உள்ள எதையும் தன்னுடன் இணைத்துக் கொள்ளும்; இதில் மிகப்பெரிய parsed arrays, பெறப்பட்ட JSON blobs அல்லது DOM trees-க்கான குறிப்புகள் (references) இருக்கலாம். ஒவ்வொரு முறை வழிசெலுத்தப்படும்போதும் (navigation) கூடுதல் சுமை சேர்கிறது. பயனர் தனது பிரவுசர் டேப் (browser tab) ஏன் 800MB நினைவகத்தைப் பயன்படுத்துகிறது என்று தெரியாது. ஆப் மெதுவாகச் செயல்படுவதையும், இறுதியில் நின்றுவிடுவதையும் மட்டுமே அவர்கள் அறிவார்கள்.

Dependency arrays ஒவ்வொரு render செய்யும் போதும் மாறும்போது இது மிகவும் ஆபத்தானது. ஒவ்வொரு சுழற்சியிலும் (cycle) ஒரு புதிய function reference உருவாகி, ஒரு listener-இல் பதிவு செய்யப்படுகிறது, ஆனால் பழையது ஒருபோதும் விடுவிக்கப்படுவதில்லை. இதன் விளைவாக, ஒவ்வொரு முறையும் தான் பிறந்தபோது இருந்த தரவுகளைச் சேமித்து வைக்கும் ஒரு "இறந்த closures-களின் அருங்காட்சியகம்" உருவாகிறது.

Dynamic Modules-இல் TDZ பிழைகள்

The Temporal Dead Zone is not a theoretical edge case. When you access a let or const before its declaration executes, the engine throws a ReferenceError. In large monorepos with circular dependencies and dynamic imports, the exact execution order is often implicit. Module A imports Module B, which dynamically imports a chunk that depends back on Module A. If one branch touches a variable that has not finished initializing, the app crashes during load. These failures are maddening because they are timing-dependent. A small change in the bundler split points, a network delay in code-loading, or a shift in chunk caching can alter the order just enough to trigger the TDZ. The crash is unpredictable, and the stack trace usually points to a perfectly innocent line of code.

Defensive Tactics

You cannot rely on stack traces to save you from scope bugs. You need prevention and detection.

Start with static analysis. Configure ESLint to enforce strict boundaries. Rules like no-implicit-globals and no-shadow catch the obvious sins. Shadowing is particularly treacherous because it tricks you into thinking you are mutating a local variable when you are actually building a closure over an outer one, or creating an accidental duplicate. These rules force explicit intent and eliminate silent collisions.

Profile your memory with the same discipline you apply to unit tests. Open Chrome DevTools, take a heap snapshot on your starting route, navigate through your application for five minutes, and take another. Compare the two. Filter for "Closure" and look for counts that grow without bound. Look for detached DOM nodes that still retain event listeners. If the second snapshot shows thousands of new Closure entries while your user count stayed flat, you have trapped functions holding trapped data. That is your leak.

Architecturally, stop reaching into the global window object for configuration. Pass settings as props or through a typed context. Dependency injection is not an enterprise buzzword here; it is the practice of giving a function everything it needs through arguments rather than letting it sniff the global scope. The result is code you can test without browser shims, and modules that do not collide when multiple apps mount inside the same shell.

Finally, respect the cleanup phase ruthlessly. Every addEventListener needs a matching removeEventListener inside the effect cleanup. For asynchronous work, use an AbortController and pass its signal to fetch so in-flight requests cancel when the component dies. These habits directly control how long a scope lives. They are not boilerplate. They are memory management.

What This Means for Your Team

Scope is not a parlor trick to quiz candidates with during interviews. In production, scope is memory management. Every variable you declare is a potential hostage. Every closure is a promise the engine will keep. When you forget to release a listener, you are not leaving a light on. You are chaining a weight to your app and dropping it in the ocean. On powerful hardware, the app swims anyway. For users on low-end devices, it sinks. Start treating scope like the finite resource it is. Your users, and your three-hour debugging sessions, will thank you.