JavaScript Global Variables: Scope and Best Practices
Learn how global scope differs from global-object properties in scripts and modules, how accidental globals happen, and when to use globalThis.
A JavaScript variable can be in global scope without being a property of the global object. In a classic browser script, top-level var and function declarations traditionally create global-object properties, while top-level let, const, and class create global lexical bindings. In CommonJS and native ES modules, top-level declarations stay within the module. For new code, keep state in the narrowest scope that needs it and share it through imports and exports. Use globalThis only when you deliberately need a host-wide global property.
1. What is a global variable in JavaScript?
“Global variable” is often used for two related but different things:
- A global binding is a name available in the global environment.
- A global-object property is a property on the host’s global object, such as a property you can access through
globalThis.
In classic browser scripts, top-level var and function declarations traditionally add properties to the global object. Top-level let and const are global lexical bindings, but they do not become properties such as window.name. In modules, top-level declarations are module-scoped. These distinctions are described in MDN’s references for var, JavaScript grammar and types, and modules.
2. How top-level declarations behave by context
| Context | Top-level declaration | Global-object property? |
|---|---|---|
| Classic browser script | var x or function f() {} |
Traditionally yes |
| Classic browser script | let x, const x, or class X {} |
No; global lexical binding |
| CommonJS module | var, let, const, function, or class |
No; module-scoped |
| Native ES module | var, let, const, function, or class |
No; module-scoped |
“Top level” means the outermost level of the current script or module. It does not mean the same scope in every execution mode. Browser workers also have their own global environment, not the page’s window. Node.js provides its own host environment; avoid assuming that a browser global such as window exists there.
Classic browser script example
<script>
var legacyCount = 1;
let pageCount = 2;
console.log(globalThis.legacyCount); // 1
console.log(globalThis.pageCount); // undefined
console.log(pageCount); // 2
</script>
The lexical name pageCount is available to code in the applicable global lexical environment, but it is not a property on globalThis. Avoid redeclaring global lexical names across separate classic scripts; declarations can conflict.
CommonJS example
// counter.cjs
const initialCount = 2;
function nextCount() {
return initialCount + 1;
}
module.exports = { nextCount };
The names declared in this file are local to its module wrapper. Another CommonJS file uses the exported interface rather than reading a global property.
Native ES module example
// counter.mjs
const initialCount = 2;
export function nextCount() {
return initialCount + 1;
}
// app.mjs
import { nextCount } from "./counter.mjs";
console.log(nextCount());
Imports are bindings in the importing module; they do not add the imported name to the global namespace. MDN notes that module features are imported into the scope of a single script rather than being available in global scope: JavaScript modules.
3. Why is my variable on window or globalThis?
First identify how the code is loaded. A top-level var or function declaration in a classic browser script can create a property accessible through window (which is the browser’s familiar global object) or globalThis. A top-level let or const does not. A declaration in a CommonJS or ES module stays module-scoped.
To inspect a suspected property, check the property directly:
console.log(Object.hasOwn(globalThis, "legacyCount"));
console.log(globalThis.legacyCount);
To check whether an identifier can be resolved, reference it only if you know it exists, or guard access with typeof:
if (typeof optionalIntegration !== "undefined") {
optionalIntegration.start();
}
These checks answer different questions: a global lexical binding can exist without being an own property of globalThis.
4. How accidental globals happen
In non-strict code, assigning to a name that has no declared binding can create an accidental global property. A misspelling can therefore leak state beyond the function that contains the assignment.
function calculateTotal() {
// Typo: meant to declare or update `total`.
totla = 4;
}
calculateTotal();
console.log(globalThis.totla); // May be 4 in sloppy-mode code
Strict mode throws a ReferenceError for assignment to an undeclared identifier instead of creating an implicit global. Native ES modules are automatically strict; classic scripts can opt in with "use strict". See MDN’s guide to strict mode.
"use strict";
function calculateTotal() {
totla = 4; // ReferenceError: totla is not defined
}
Strict mode catches this class of mistake, but it does not prevent deliberate global-object writes, nor does it replace careful scoping.
5. Best practices for sharing state
- Keep values local. Declare a value inside the smallest function or block that needs it.
- Use modules for shared code. Export a small interface and import the names that depend on it.
- Declare every binding. Use
constwhen the binding will not be reassigned andletwhen it must be. Use block-scoped declarations instead ofvarin new code. - Remember what const means. It prevents rebinding the variable; it does not freeze an object referenced by that variable.
- Use strict behavior. ES modules are strict automatically. For classic scripts, consider a strict-mode directive.
- Lint for mistakes. ESLint’s
no-implicit-globalsrule can help find unintended global declarations or assignments. Check the rule’s behavior against the project’s script and module configuration.
Prefer an explicit module interface
// settings.js
export const settings = {
retryLimit: 3,
};
// worker.js
import { settings } from "./settings.js";
export function shouldRetry(attempt) {
return attempt < settings.retryLimit;
}
This makes the dependency visible in the importing file. A global variable hides where a value came from and lets unrelated code change it.
6. When to use globalThis
globalThis provides a standard way to access the global this value across JavaScript environments. A host may define its value with environment-specific semantics, so do not assume every host’s global behaves exactly like a browser’s window. See MDN’s globalThis reference.
Use it when integrating with a host-wide API or when a deliberate compatibility layer needs one well-named shared property. Keep the namespace specific, define ownership and lifetime, and avoid using it as a general application-state container.
// Deliberate host integration, for code that needs a shared bridge:
globalThis.AppBridge = {
version: 1,
notify(message) {
console.log(message);
},
};
Do not use this to work around module imports. If one module needs a value from another, export and import it.
7. Block scope, functions, and closures
Global scope is only one layer of JavaScript’s scope rules. A function can access bindings from surrounding scopes through lexical scope, and a block creates scope for let and const.
const limit = 10;
function makeCounter() {
let count = 0;
return function increment() {
count += 1;
return Math.min(count, limit);
};
}
const increment = makeCounter();
console.log(increment()); // 1
count is private to the closure; it does not need to become global to persist between calls. limit is in the surrounding module or script scope, depending on how the code runs. A closure is often the right way to keep state available to a small set of functions without exposing it everywhere.
8. Troubleshooting common global-variable problems
| Symptom | Likely cause | Fix |
|---|---|---|
window.myValue is undefined, but myValue works |
It was declared with top-level let or const in a classic script. |
Use the binding directly, or deliberately assign a documented property to globalThis if an external consumer requires one. |
| A name is unavailable in another file | The files are modules, or the declaration is function/block-scoped. | Export it from its module and import it where needed. |
ReferenceError: x is not defined after a typo |
The code assigns to an undeclared name; strict mode catches it. | Correct the spelling and declare the intended binding with const or let. |
Identifier 'x' has already been declared |
A lexical name was declared more than once in the same scope, often across classic scripts. | Give the binding one owner, wrap code in a module or function, or rename the conflicting declaration. |
| Third-party script overwrites an application value | Multiple scripts share a global-object property name. | Use modules where possible; otherwise use one project-specific namespace and coordinate ownership. |
| Code works in a browser but fails in Node.js | It depends on browser-only globals such as window, or relies on classic-script behavior. |
Use environment-appropriate APIs and module imports; access the shared global through globalThis only when that is actually the intended interface. |
9. Performance, reliability, and maintenance
Choosing a global binding is primarily a design and correctness decision. The practical costs are hidden dependencies, name collisions, unclear ownership, and tests that must account for shared mutable state. Narrow scope and explicit module interfaces make dependencies easier to trace and isolate.
For reliability, avoid mutating global state during module initialization unless it is part of a documented host integration. If a global bridge is required, give it a distinctive name, initialize it once, define who may modify it, and handle the case where another script has already defined it. Do not assume that a value on the global object is safe to overwrite.
10. Or skip the browser setup
If you are building documentation or a visual check around JavaScript examples, you can capture a page with ScreenshotNeo using one GET request. See the ScreenshotNeo API documentation for options and setup.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Grammar_and_types -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Grammar_and_types",
},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as image:
image.write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Grammar_and_types',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Learn about ScreenshotNeo, then sign up free for 1,000 screenshots a month, with no card.
11. FAQ
Does declaring a variable at the top of a file make it global?
Only relative to that file’s execution context. It is global in a classic script environment, but module top-level declarations are module-scoped.
Is a global lexical binding the same as a property on window?
No. A top-level let or const in a classic script can be globally scoped without appearing as window.name or globalThis.name.
Does const make an object immutable?
No. It prevents assigning a different value to the binding. Properties of the referenced object can still be changed unless separately protected.
Should I use globalThis or export a value?
Use exports and imports for dependencies between modules. Use globalThis when a host integration intentionally requires a shared global property.


