A bug report arrived that defied every debugging instinct. Users on low-end Android phones said the app simply disappeared. Not at launch. Not during a specific tap or swipe. Roughly twenty minutes into a session, the screen froze and the process died. The logs were spotless. QA could not reproduce it on their high-end hardware. There were no steps to follow. After three hours of memory profiling, the picture finally cleared. A single event listener sat inside a React hook. That listener had closed over a large dataset. The component unmounted. The listener stayed. The dataset stayed in memory. On a device with 2GB of RAM, that accumulation exhausted the heap and the operating system killed the app. This was not a syntax error or a logic flaw. It was a scope bug, and it was fatal.
How a Closure Becomes a Leak
Most tutorials teach scope as an academic puzzle about where a variable is visible. In production, scope is a contract about memory lifetime. When a JavaScript function closes over a variable, the engine keeps that variable alive as long as the closure itself can be reached. In a React component, this means your data survives long after the user navigates away and the UI node is gone.
Consider a hook that registers a listener on the window object. The component renders, attaches the listener, and later unmounts. If the cleanup phase is missing or botched, the listener remains. Every fresh mount adds another ghost copy of the trapped data to RAM. On a developer workstation with abundant memory, you might never notice the bloat. On a budget phone running Android Go, twenty minutes of ordinary use is enough to exhaust the available heap. The OS steps in and terminates the process. There is no exception to log. The system simply pulls the plug.
This is why scope is memory management. The lexical environment is not a philosophical boundary. It is a retention graph. Every variable you leave inside an uncollected closure is a brick in a wall that eventually boxes your app in.
Three Ways Scope Kills Production Apps
Scope issues do not all look alike. Some drain memory slowly. Others explode instantly. Here are the patterns that reliably bring apps down.
Global Scope Pollution
Micro-frontend architectures let teams ship independently, but they all share the same window object. When one application sets a global variable like window.config or patches a shared utility onto window, it does not live in isolation. Another team’s app might depend on a different shape for that same global, or overwrite it during its own bootstrap. The result is a feature collision that scales with your organization. A developer in one repository has no idea their shortcut is another team’s breaking change. As the surface area grows, these globals become landmines buried in shared soil.
Closure Memory Leaks
Single page applications are built to run for hours. That durability is exactly why leaking closures become toxic. The pattern is deceptively common: a useEffect registers a callback with a global event bus, a WebSocket handler, or the DOM itself. If the dependency array is unstable or omitted, the cleanup never matches the original subscription. The closure captures whatever is in its lexical scope, which can include massive parsed arrays, fetched JSON blobs, or references to DOM trees. Every navigation adds more weight. The user does not know why their browser tab is pushing 800MB. They only know the app feels sluggish and eventually dies.
This is especially dangerous when dependency arrays change on every render. A new function reference is born each cycle, registered with a listener, and the old one is never released. The result is a museum of dead closures, each hoarding the data they were born with.
TDZ Errors in Dynamic Modules
Temporal Dead Zone (TDZ) teorik bir uç durum değildir. Bir let veya const değişkenine, bildirimi henüz yürütülmeden erişmeye çalıştığınızda, motor bir ReferenceError fırlatır. Döngüsel bağımlılıkları ve dinamik içe aktarmaları (dynamic imports) olan büyük monorepo'larda, tam yürütme sırası genellikle örtüktür. Modül A, Modül B'yi içe aktarır; Modül B ise tekrar Modül A'ya bağımlı olan bir parçayı (chunk) dinamik olarak içe aktarır. Eğer bir dal, henüz başlatılması tamamlanmamış bir değişkene dokunursa, uygulama yükleme sırasında çöker. Bu hatalar, zamana bağlı oldukları için sinir bozucudur. Paketleyici (bundler) ayırma noktalarındaki küçük bir değişiklik, kod yüklemedeki bir ağ gecikmesi veya chunk önbelleğe alma yöntemindeki bir kayma, sırayı TDZ'yi tetikleyecek kadar değiştirebilir. Çökme öngörülemezdir ve yığın izi (stack trace) genellikle tamamen masum bir kod satırını işaret eder.
Savunmacı Taktikler
Kapsam (scope) hatalarından kurtulmak için yığın izlerine güvenemezsiniz. Önlemeye ve tespit etmeye ihtiyacınız var.
Statik analiz ile başlayın. ESLint'i katı sınırlar uygulaması için yapılandırın. no-implicit-globals ve no-shadow gibi kurallar bariz hataları yakalar. Shadowing (gölgeleme) özellikle tehlikelidir; çünkü aslında dıştaki bir değişken üzerinde bir closure oluştururken veya yanlışlıkla bir kopya yaratırken, yerel bir değişkeni değiştirdiğinizi sanmanıza neden olur. Bu kurallar açık niyet beyanını zorunlu kılar ve sessiz çakışmaları ortadan kaldırır.
Belleğinizi, birim testlerine uyguladığınız disiplinle profilleyin. Chrome DevTools'u açın, başlangıç rotanızda bir heap snapshot alın, uygulamanızda beş dakika gezinin ve bir tane daha alın. İkisini karşılaştırın. "Closure" filtresini uygulayın ve sınırsızca artan sayıları arayın. Hala olay dinleyicilerini (event listeners) tutan ayrılmış (detached) DOM düğümlerine bakın. Eğer kullanıcı sayınız sabit kalırken ikinci snapshot binlerce yeni Closure girişi gösteriyorsa, hapsolmuş verileri tutan hapsolmuş fonksiyonlarınız var demektir. Sızıntınız budur.
Mimari açıdan, yapılandırma için global window nesnesine uzanmayı bırakın. Ayarları prop'lar aracılığıyla veya tiplendirilmiş bir context üzerinden iletin. Bağımlılık enjeksiyonu (dependency injection) burada kurumsal bir moda sözcük değildir; bir fonksiyonun global kapsamı koklamasına izin vermek yerine, ihtiyacı olan her şeyi argümanlar aracılığıyla ona verme pratiğidir. Sonuç; tarayıcı shim'lerine ihtiyaç duymadan test edebileceğiniz kodlar ve birden fazla uygulama aynı kabuk (shell) içinde çalıştığında çakışmayan modüllerdir.
Son olarak, temizleme (cleanup) aşamasına acımasızca saygı duyun. Her addEventListener için effect temizliği içinde eşleşen bir removeEventListener gereklidir. Asenkron işler için bir AbortController kullanın ve sinyalini fetch fonksiyonuna iletin; böylece bileşen yok olduğunda devam eden (in-flight) istekler iptal edilir. Bu alışkanlıklar, bir kapsamın ne kadar süre yaşayacağını doğrudan kontrol eder. Bunlar basmakalıp kod (boilerplate) değildir. Bunlar bellek yönetimidir.
Bu Ekibiniz İçin Ne Anlama Geliyor?
Kapsam (scope), mülakatlarda adayları terletmek için kullanılan bir gösteriş numarası değildir. Canlı ortamda (production) kapsam, bellek yönetimidir. Tanımladığınız her değişken potansiyel bir rehinedir. Her closure, motorun tutacağı bir sözdür. Bir dinleyiciyi (listener) serbest bırakmayı unuttuğunuzda, sadece bir ışığı açık bırakmış olmazsınız. Uygulamanıza bir ağırlık zincirleyip onu okyanusa bırakmış olursunuz. Güçlü donanımlarda uygulama yine de yüzer. Ancak düşük donanımlı cihazlardaki kullanıcılar için uygulama batar. Kapsamı, olduğu gibi sınırlı bir kaynak olarak görmeye başlayın. Kullanıcılarınız ve üç saat süren hata ayıklama (debugging) seanslarınız size teşekkür edecektir.
