JavaScript Refresher and Detailed Study - Part 11

Last updated on 24 Oct 2026
Post series: Part 1,     Prev Part (10),     Post Series TOC

Contents


Gemini chats: JavaScript-Refresher-P11-20261021

  • Model: Pro 3.1

Dates: 21 Oct, 23 to 24 Oct 2026


Generator functions and yield - function* fnName(){...} and yield <value> syntax

Me: Now I am going through generators (https://javascript.info/generators#generator-functions). This is my first exposure to them. Here's a modified example code that runs as expected.

'use strict';
// Based on https://javascript.info/generators#generator-functions

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

let one = generator.next();

console.log(JSON.stringify(one)); // {value: 1, done: false}

let two = generator.next();

console.log(JSON.stringify(two)); // {value: 2, done: false}

let three = generator.next();

console.log(JSON.stringify(three)); // {value: 3, done: true}

let four = generator.next();

console.log(JSON.stringify(four)); // {"done":true}

(I improved this code later on. The file in private repo is generators-1.js.)

Me: Modified the code (see uploaded file) to capture the JSON.stringify() quirk. Pl. see the modifications and tell whether its OK.

... (Next files were generators-2.js, generators-3.js. IIRC, I also created test files related to using generators for iterables and generator composition but I do not have access to those files as of now. Plan to update this later when I do get that access.)

Two-way yield is rarely used in modern JS apps; async/await has replaced use of two-way generators for writing asynchronous code that 'looks synchronous'

Me: Now I am looking at “yield” is a two-way street https://javascript.info/generators#yield-is-a-two-way-street

Programming feature wise, it looks extraordinary. But I am wondering how much time I should spend on this. You see, AFAIK none of the ReactJS and Vanilla JS apps I read in past 2 to 3 years, used function generators or yield.

Gem: You can safely treat this section as a quick conceptual read and move on. Your observation is spot on—you haven't seen two-way yield in the React and Vanilla JS codebases you've been reviewing because it has largely become a historical artifact in mainstream web development.

... Here is why this specific feature rarely shows up in modern code:

  • The async/await Takeover: Before ES2017, the primary way to write asynchronous JavaScript that looked synchronous was by using two-way generators paired with runner libraries (like co). You would yield a Promise, and the library would pass the resolved data back in via .next(data). Once async/await became the native standard, it completely cannibalized this use case.
  • React Architecture Shifts: In the older React ecosystem, a state management middleware called Redux Saga heavily relied on two-way yield for handling complex side effects. Modern React development has mostly abandoned this in favor of standard async functions or dedicated data-fetching hooks (like React Query), pushing generators out of everyday component logic.
  • Niche Natively: Today, actively passing values back into a generator via generator.next(value) is mostly reserved for specialized library authoring, streaming massive datasets, or building complex custom state machines.

Understanding the coroutine concept is a great addition to your overall software engineering mental model, but for building practical Vanilla JS PWAs or React Native utilities today, you will almost certainly never need to author a two-way generator yourself.

for await...of loop and async generators are not commonly used in frontend apps, but are useful for massive or continuous data pipelines

Me: Another fascinating topic - Async iteration and generators https://javascript.info/async-iterators-generators

But never came across it in my reading of React JS and Vanilla JS apps. I plan to get the concepts and then move on. Ur view pl.

Gem: Your plan to grab the high-level concepts and keep moving is the perfect strategy. Async iterators and generators are brilliant for very specific engineering problems, but they are practically invisible in standard UI development.

In the React and Vanilla JS codebases you typically work with, standard async/await, Promise.all(), and simple fetch requests handle almost all data fetching. Frontend applications usually request discrete, complete JSON payloads rather than continuous streams.

The for await...of loop and async generators really shine when dealing with massive or continuous data pipelines:

  • Node.js Backends: Reading gigabyte-sized files chunk by chunk without overflowing system memory.
  • Database Cursors: Paginating through massive datasets sequentially on a server.
  • Browser Streams: Manually processing custom Fetch API ReadableStream chunks, such as reading a live video feed, streaming AI text generation, or handling server-sent events.

Understanding that Symbol.asyncIterator exists gives you the necessary architectural context. Skipping the deep dive wisely preserves your flexi-time hours for mastering the standard asynchronous patterns you actually use to build and deploy applications.

In modern React JS apps, app files are usually modules but some exception cases may need loading script(s) as global (not modules)

Me: I am going through https://javascript.info/modules-intro now.

In modern React JS apps, all files are modules I believe. Am I right?

Gem: You are absolutely right. In modern React development, every component, hook, and utility file within your source directory operates as a strictly independent module.

Unlike older monolithic scripts where variables could accidentally leak into the global environment, files in a modern React project have isolated local scope. You must explicitly export what you want to share and import what you need.

When you build applications using tools like Next.js or React Native via Expo, the underlying bundlers (like Webpack, Rollup, or Metro) automatically treat every .js, .jsx, or .ts file as a module. They trace your import and export statements to map out the application's dependency tree and bundle it together efficiently. This is why you rarely have to manually declare <script type="module"> in a root HTML file when working in these frameworks; the build system orchestrates the entire module graph for you behind the scenes.

The only exception you might occasionally spot is a traditional global script manually added to the public index.html via a standard <script src="..."> tag (such as an older third-party analytics snippet), but your actual application code is entirely modular.

In ReactJS apps, underlying bundlers like Webpack generate custom JS code to manage and load .js, .jsx and .ts files, and strip out import and export statements; Network efficiency is main advantage

Me: U wrote: "the underlying bundlers (like Webpack, Rollup, or Metro) automatically treat every .js, .jsx, or .ts file as a module."

Can you explain that in more detail? I thought the module declaration is a JS language aspect. How can the bundler view a .js file as a module even if it is not loaded explicitly as a module? Is the bundler generating some JS code that explicitly loads other .js files as modules?

Gem: Yes, your intuition is exactly right—the bundler generates its own custom JavaScript code to manage and load these files, completely bypassing the browser's native module loading mechanism.

Think of a JavaScript bundler exactly like a C++ compiler and linker. When writing C++, you have multiple source files connected via #include directives. The linker resolves all those references at build time and stitches them into a single executable. The operating system doesn't know or care about the individual .cpp files; it just executes the final binary. Modern JavaScript bundlers do the exact same thing for web and mobile apps.

Here is exactly what the bundler is doing under the hood when it sees your .js files:

  • Parsing the Graph: At build time (using Node.js), the bundler starts at your entry file (like index.js or App.js) and scans for the import keyword. It uses this native JavaScript language feature purely as an instruction manual to build a massive dependency graph of every file your app needs.
  • Wrapping for Isolation: To mimic the isolated scope of a native module, the bundler takes the code from each of your files and wraps it inside an ordinary JavaScript function. This guarantees that variables in fileA.js don't accidentally leak and collide with variables in fileB.js, achieving module isolation without using <script type="module">.
  • Injecting the Custom Loader: The bundler injects a tiny, custom runtime function into the final output (Webpack, for example, uses one called __webpack_require__). It then rewrites all your native import and export statements to use this custom function instead.
  • Emitting the Bundle: Finally, it outputs a single, massive standard JavaScript file (or a few "chunks").

When this final bundle is served to the browser or the React Native JavaScript engine, it is just loaded as a standard, old-school script. The browser never sees the original 50 individual modules or native import statements; it just executes the bundler's giant script, which manages its own internal map of wrapped functions.

The native ES module specification (<script type="module">) exists so browsers can load modules directly without needing a build step at all. However, because making hundreds of individual network requests for tiny files is too slow for production apps, the industry relies on bundlers to compile them into a single optimized package beforehand.

...

Me: Does the bundler strip out the import statements in React JS code in the final JS bundle it creates which is what the browser sees?

Gem: Yes, the bundler completely strips out all native import and export statements in the final generated bundle.

If a browser were to encounter an import keyword inside a standard <script> tag that lacks the type="module" attribute, it would immediately throw a syntax error. To prevent this, the bundler translates those module declarations into standard JavaScript variable assignments and function calls.

When the bundler parses a statement like import React from 'react';, it removes that line entirely and replaces it with a call to its injected runtime loader. In the final bundle, it looks something like this:

var React = __webpack_require__("./node_modules/react/index.js");

Just as a C++ compiler resolves and removes #include directives before generating the final binary, replacing them with linked memory addresses, the JavaScript bundler removes the import directives and replaces them with direct references to the wrapped function scopes it created for each file. The browser merely executes a large script where variables are being passed between functions, completely unaware that ES modules were ever involved in the source code.

Me: My God! That is a big transformation that these bundlers do. I mean, if I write a Vanilla JS app without any bundlers and I use modules in it, the import and export statements would be seen and interpreted by the browser. So I use the JS language feature in my code and that feature is interpreted by the JS engine in the browser. In contrast, in a React JS app, the browser does not see the import statements I write!

Gem: You have hit the exact realization that separates modern framework development from traditional scripting. It is a massive transformation, and your C++ compiler analogy from earlier is exactly why thinking of it as a "build step" makes so much sense.

When you write a Vanilla JS app with <script type="module">, you are writing true, native JavaScript. The browser's JS engine (like Chrome's V8) reads your code, sees the import statement, pauses execution, makes an actual network request to fetch the imported file, and links it natively in memory. You are utilizing the runtime environment exactly as it was designed.

With a bundled React app, you aren't really writing code for the browser anymore—you are writing source code for the build tool. You are using import and export purely as architectural blueprints. The bundler reads those blueprints, strips out the native language features, and engineers a custom delivery mechanism that completely bypasses the browser's native capabilities.

This divergence happened out of pure necessity. The module standard was finalized in 2015, but it took years for browsers to actually support it reliably. Toolmakers had to invent bundlers to bridge that gap. By the time browsers could finally read an import statement natively, the industry had realized that sending one bundled payload was vastly faster over the network than forcing the browser to make hundreds of individual HTTP requests for tiny React component files. So, the "hack" became the permanent industry standard.

Me: Oh I see! Good to see the explanation about efficiency being a key reason that in React JS app, the import and export are transformed by the bundler, even if modern day browsers support them. Did I get this correctly?

Gem: You got it exactly right. Network efficiency is the foundational reason this pattern took over. Downloading one compressed payload is vastly faster than forcing the browser to negotiate hundreds of individual HTTP requests for every tiny component and utility file.

Even today, with modern browsers natively supporting import, bundlers remain essential because they optimize the code in ways the browser cannot do on its own:

  • Tree-Shaking: Bundlers analyze your dependency graph and automatically delete library functions you imported but never actually used, keeping the final file size as small as possible.
  • Translation: Browsers do not natively understand React's JSX syntax or TypeScript. The bundler pipeline transforms those non-standard formats into plain Vanilla JS on the fly.

So you have the perfect mental model: you use modules to keep your source code clean and architecturally sound, and the bundler transforms that architecture into a highly optimized delivery vehicle for the end user.

For small vanilla JS apps with one main app source file and possibly one service worker source file, global script is preferred over modular script

Me: Now let us look at Vanilla JS apps. Some weeks back, using Gemini help, I developed a Text Scratchpad vanilla JS PWA app. It has an index.html, an app.js and sw.js as its main files. But index.html does not load app.js as a module! So even though you generated the code, you chose to not use modules. Ur comments pl.

Gem: You caught that perfectly. When generating the architecture for the Text Scratch Pad app, I deliberately skipped the ES module system for three specific technical reasons optimized for lightweight Vanilla JS development:

  • The Local Testing Hurdle (CORS): If you add type="module" to a script tag, the browser strictly enforces CORS (Cross-Origin Resource Sharing) policies. This means if you simply double-click index.html to open it locally via the file:// protocol, the browser will block the JavaScript from running. You would be forced to spin up a local development web server just to see your app. By using a standard script, it allowed you to test the UI instantly without any extra tooling.
  • Single-File Simplicity: Because the core application logic was contained entirely within app.js, there were no external dependencies to pull in. The module system's primary benefits—namespace isolation and dependency mapping—become redundant when you only have one file.
  • Service Worker Architecture: A Progressive Web App's Service Worker (sw.js) operates in a completely separate, isolated background thread. It cannot directly import from your main DOM scripts anyway. Historically, Service Workers used their own distinct loading mechanism (importScripts), keeping the PWA infrastructure separate from the main UI thread's module graph.

In a heavy framework ecosystem like React, the bundler hides the local server requirements and module complexity from you. For a streamlined Vanilla JS app engineered for tools like PWABuilder, utilizing the global scope with standard scripts is usually the fastest, most frictionless path to a working prototype.

Me: Hmm. So if the app is quite simple with one single .js file having main app code reflecting its simplicity, it is not worth going for the extra work involved in using modules. It is when the plain vanilla app gets complex with functionality spread across source files, that modules may be more appropriate to consider. Is my understanding on the right track here?

Gem: You have nailed the exact architectural threshold. For a simple project where all the logic fits comfortably in a single file, a standard script is the most efficient and frictionless approach. It keeps the architecture lean and avoids local testing roadblocks like CORS.

The moment your Vanilla JS app grows enough to require separating logic across multiple files, crossing over to native ES modules (<script type="module">) becomes the best practice. Once you reach that complexity, you need the module system to handle:

  • Namespace Protection: Modules prevent variables in a file like ui.js from accidentally colliding with variables in data.js, keeping the global window object clean.
  • Execution Order: Using import ensures your scripts load and execute in the correct sequence automatically, rather than forcing you to carefully stack multiple <script> tags in a specific order in your HTML file.
  • Encapsulation: It becomes much easier to isolate, debug, and reuse specific features when they are explicitly contained and exported from their own files.

It is entirely about scaling the architecture to match the complexity of the application. You perfectly identified the tipping point.

Modern web security heavily restricts inline scripting to prevent Cross-Site Scripting (XSS) attacks

Me: Theoretically/technically, inline scripts can also be modules. But I don't think I have seen that in app codebases I read in past 2 or 3 years. Ur comments pl.

Gem: You are completely right; while <script type="module"> can absolutely enclose inline JavaScript, it has been practically eradicated from modern production codebases.

  • The Bundler Shell: In the React and structured Vanilla JS architectures you have been reviewing, the index.html is usually just an empty mounting shell. The build tools automatically inject an external <script src="..."> tag pointing to the compiled assets. Manually writing inline modules bypasses this entire automated pipeline.
  • Strict Security Policies: Modern web security heavily restricts inline scripting to prevent Cross-Site Scripting (XSS) attacks. A standard Content Security Policy (CSP) will block unsafe-inline scripts entirely, forcing developers to extract all JavaScript into separate, verifiable external files.
  • Caching Inefficiencies: Browsers aggressively cache external .js files. If module logic is written inline within the HTML, the browser is forced to download that code every single time the HTML page is requested, completely defeating browser caching strategies.
  • Tooling Breakdown: Writing JavaScript directly inside HTML files breaks the modern developer experience. VS Code features, Prettier formatting, and code-assist extensions struggle to provide accurate intellisense and linting when JavaScript is not isolated in its own .js or .jsx file.

It is technically valid according to the ECMAScript specification, but the modern ecosystem's demands for security, performance, and tooling have made it a complete anti-pattern.


Module scripts are always deferred; Classic (global) scripts can be deferred and placed near start of HTML page to get parallel load of script and HTML parsing

23 Oct 2026

Me: ... https://javascript.info/modules-intro states:

"Module scripts are always deferred, " ---snipped---

What I noticed in vanilla JS apps I read over past 2 to 3 years, is that the main app script loading, whether module or not, was almost always, if not always, at the end of the HTML. So the scripts would see the whole HTML loaded before it.

Ur thoughts pl.

Gem: Your observation is completely accurate, and it highlights a fascinating collision between deeply ingrained developer habits and modern browser capabilities.

For over a decade, placing the <script> tag right before the closing </body> tag was the absolute gold standard for web development. Because traditional JavaScript is parser-blocking, if a browser encountered a script in the <head>, it would freeze HTML rendering, download the file, and execute it before drawing the rest of the UI. Putting the script at the bottom guaranteed the DOM was fully built so methods like document.getElementById() wouldn't return null, preventing the app from crashing while ensuring the user saw the visual page as quickly as possible.

However, because type="module" automatically applies the defer behavior under the hood, putting module scripts at the bottom of the HTML is actually a legacy habit.

Here is how modern mechanics change the game:

  • The Optimal Modern Placement: The best place for a <script type="module"> (or a standard script with the explicit defer attribute) is actually up in the <head>.
  • Parallel Downloading: When placed in the <head>, the browser discovers the script immediately and fetches it over the network in the background while simultaneously parsing the HTML body.
  • Guaranteed Execution Order: Even though it downloads early, the deferred nature guarantees the engine will not actually execute the module until the entire DOM is completely built. It achieves the exact same DOM safety as putting the script at the bottom.

Many developers still place scripts at the bottom out of pure muscle memory. But technically, by putting a module at the bottom of the document, you force the browser to wait until it finishes reading the entire HTML file before it even starts the network request to download your JavaScript, leaving free network performance on the table.

Me: But most vanilla JS apps I read, IIRC, did not use type=module. In that case, placing the app script load at the bottom of the HTML makes sense, doesn't it?

Gem: You are exactly right; without type="module" or an explicit defer attribute, placing the script at the very bottom of the HTML is absolutely the correct and necessary approach.

When the browser encounters a standard <script src="..."> tag, it immediately halts all HTML parsing. If that tag sits in the <head>, the browser freezes, downloads the file, and executes the JavaScript before it even begins drawing the visual page. This guarantees a blank white screen for the user and immediately crashes any JavaScript trying to access DOM elements that haven't been created yet. Placing the script right before the closing </body> tag ensures the DOM is fully constructed and the UI is visible before the script runs.

However, even for those non-module Vanilla JS apps, developers today often place standard scripts up in the <head> but explicitly add the defer attribute (<script src="app.js" defer></script>). This achieves the exact same parallel-download efficiency as modules while ensuring the script waits for the DOM to finish building. But if you see a script missing both type="module" and defer, the bottom of the document is exactly where it belongs.

Scenarios where a standard non-deferred script is loaded in <head>: Preventing UI flashes for dark mode, critical polyfills and feature detection, security and redirection, and early analytics

Me: Got the point that defer brings in parallel loading. Any other reason why non-module scripts should be loaded towards beginning of HTML page?

Gem: Beyond the network efficiency of parallel downloading, loading a standard, non-deferred script in the <head> is done when you explicitly need to block the HTML parser. You do this when the script must alter the global environment before the browser draws a single pixel.

Here are the primary architectural reasons you would intentionally force a script to execute before the DOM loads:

  • Preventing UI Flashes (FOUC): If an application supports a "dark mode" based on user preference or local storage, placing that check at the bottom of the document means the browser will paint the default white screen first, and then suddenly flash to dark once the script runs. A tiny script in the <head> evaluates the preference and applies the CSS class before the <body> is even rendered.
  • Critical Polyfills and Feature Detection: If the rest of the application relies on certain browser capabilities (like older libraries such as Modernizr used to do), you have to test for those features and patch the environment immediately so subsequent scripts don't crash.
  • Security and Redirection: If a page requires strict authentication, a script in the <head> can check for a valid session token and instantly redirect the user to a login page, preventing them from seeing any protected HTML layout that might otherwise render for a split second.
  • Early Analytics: Marketing and analytics trackers often want to register a "page hit" the millisecond the routing begins, ensuring they capture the data even if the user closes the tab before the full DOM finishes downloading.

For streamlined apps like the Text Scratch Pad PWA you built, you didn't need any of these pre-render interventions, which is exactly why standard bottom-of-the-page script loading worked flawlessly.

async script loading is rarely used for main app logic in modern JS dev but is used for independent drop-in scripts like analytics and ad network scripts

Me: Is async used with script element in modern JS code?

Gem: Yes, async is heavily used in modern web development, but almost never for the main application logic you have been reviewing. It is strictly reserved for completely independent, third-party scripts.

To understand why, you have to look at the crucial difference in how the browser handles the execution timing:

  • defer (The Polite Loader): Downloads in the background, guarantees scripts execute in the exact order they appear in the HTML, and politely waits for the entire DOM to finish building before running.
  • async (The Aggressive Loader): Downloads in the background, but the very millisecond the download finishes, it instantly halts the HTML parser and executes. It completely ignores the DOM state and ignores the order of other scripts.

Because async executes unpredictably, it is incredibly dangerous for your main Vanilla JS or React code. If app.js relies on utils.js, but app.js happens to download slightly faster, an async attribute will execute it first, immediately crashing your application. It also cannot guarantee the DOM elements your app needs to manipulate even exist yet.

However, async is the absolute standard for drop-in scripts that do not care about your DOM or your other code. You will see it constantly used for:

  • Analytics: Google Analytics or tracking pixels want to fire as fast as humanly possible to record the visit, regardless of whether the page has finished rendering.
  • Ad Networks: Scripts that inject advertisements want to run immediately so the ad starts loading before the user scrolls.
  • Standalone Widgets: Things like customer support chat bubbles that operate in complete isolation from your main application architecture.

If a script is entirely self-contained and needs to run instantly, developers use async. If the script relies on the DOM or other files, they use defer (or type="module", which defers by default).

Scripts loaded as classic (global) scripts are blind execution commands that will be invoked multiple times if same script is loaded multiple times, but scripts loaded as ES modules act as strict singletons

Me: https://javascript.info/modules-intro#external-scripts says: (about external script with same src running only once)

What I find a little strange is why module script behaviour is different from non-module scripts for above case. I mean, it brings in so much confusion for a developer/learner of JS.

Gem: It can definitely feel like a jarring inconsistency when you are comparing the two side-by-side. It adds a cognitive load to the learning curve, but this specific behavior was introduced to solve one of the most fragile aspects of early web architecture.

The fundamental shift is that classic scripts are just blind execution commands, whereas ES modules are engineered to act as strict singletons.

When the browser encounters a <script type="module" src="my.js"> or an import statement, it does not just blindly run the text. It registers the file's exact URL as a unique key in an internal JavaScript engine registry called the "Module Map." If the browser sees another script tag or import requesting that exact same URL, it checks the map, sees the code has already been evaluated, and simply shares the existing instance in memory.

Think of it exactly like #ifndef / #define header guards in C++. It strictly prevents the engine from processing the same file multiple times. This is absolutely critical for two reasons:

  1. Shared State and Memory: If you have a module that initializes a configuration object or a WebSocket connection, and three different parts of your application import it, they all must talk to the exact same instance. If the module executed multiple times, each part of your app would get a completely isolated copy, breaking the shared state.
  2. Cyclic Dependencies: If Module A imports Module B, and Module B needs a utility from Module A, the application would immediately crash into an infinite loop. The "run only once" rule completely breaks this cycle.

With classic, non-module scripts, the browser acts imperatively. If you accidentally include a classic script twice, it evaluates the code twice, which almost always results in a fatal application crash due to "variable already declared" errors.

The module system's "execute once" rule might seem inconsistent with the old ways, but it makes dependency management incredibly safe and predictable.

Me: Is it that ideally even classic scripts should not have this behaviour of executing twice when included twice, but that it was already done, and could not be changed due to backward compatibility requirement of web? With modules, JS designers had the freedom to fix this issue and so they fixed it. Am I on right track here?

Gem: You are exactly on the right track. This is a perfect example of the foundational rule of browser development: "Don't break the web."

When JavaScript was created in 1995, it was designed for tiny, procedural snippets of interactivity. If a developer accidentally or intentionally included a classic script twice, the browser simply read the text top-to-bottom and executed it twice. Over the next two decades, millions of websites were built that actually relied on this exact behavior—perhaps using a repeated script to inject HTML via document.write() or to increment a global counter. If browser vendors suddenly changed classic <script> tags to execute only once, it would instantly break a massive portion of the historical internet.

Because backward compatibility is non-negotiable for browsers, language designers and web standards bodies were completely trapped by that legacy behavior.

The introduction of <script type="module"> was their escape hatch. By creating a brand new, opt-in attribute, they finally had a clean slate to implement modern software engineering principles without touching the fragile legacy ecosystem. This allowed them to fix multiple historical flaws all at once. By requiring developers to opt into type="module", designers could safely mandate that scripts act as singletons that execute only once, automatically run in use strict mode, isolate top-level variables from the global window object, and defer execution until the HTML is safely parsed.

Classic scripts can be loaded from anywhere without CORS headers, while module scripts are strictly blocked unless the cross-origin server explicitly permits it

Me: https://javascript.info/modules-intro#external-scripts says: (about external module scripts requiring CORS headers)

Hmm. Does that mean that CORS applies only to scripts loaded as modules, and that same script loaded from another origin as a classic script, will not face CORS issue?

Gem: You are exactly right. Classic scripts can be loaded from anywhere without CORS headers, while module scripts are strictly blocked unless the cross-origin server explicitly permits it.

This is another classic example of the "Don't break the web" principle in action.

  • Classic Scripts (The Legacy Way): Since the 1990s, browsers allowed standard <script src="..."> tags to fetch and execute JavaScript from any domain. This open architecture is why Content Delivery Networks (CDNs) work so seamlessly, allowing developers to drop in a React or jQuery script tag from a third-party server without touching server headers. If browser vendors suddenly applied CORS to classic scripts, millions of legacy sites relying on external widgets, ad trackers, and CDNs would instantly break.
  • Module Scripts (The Modern Security Baseline): When ES modules were introduced, web standards bodies finally had the opportunity to enforce modern security defaults. Because the module system (import) can recursively fetch dozens of other files and deeply integrate with the application architecture, they locked it down. Fetching a module from a different domain triggers a standard cross-origin HTTP request, meaning the remote server must explicitly opt-in by sending the Access-Control-Allow-Origin header.

There is one minor quirk with classic scripts: while they will successfully execute without CORS, the browser will intentionally hide their detailed execution errors (returning a generic "Script error." in the console instead of the line number) to prevent cross-site data leaks. But for actual loading and execution, classic scripts bypass CORS entirely.

...

Me: Oh yes! It was with fetch that I would typically face the CORS issues in React JS apps IIRC, and not script.

Gem: That "Aha!" moment is spot on. In almost every modern React application, fetch or axios calls are the usual suspects when you run into a CORS wall.

Here is exactly why that happens in the typical React architecture you have been working with:

  • Same-Origin Script Loading: In a React project (whether bundled by Vite, Webpack, or Metro), your compiled .js bundle and your index.html are usually served from the exact same server (e.g., both from localhost:3000 during development, or both from the same domain in production). Because they share the same origin, the browser never even thinks about CORS when loading the script.
  • Cross-Origin Data Fetching: Your frontend app, however, almost always needs to talk to a separate backend API (e.g., localhost:8080 or api.mycompany.com). The moment your React component uses fetch() to request JSON from that different port or domain, the browser's strict security model kicks in. If that backend server doesn't explicitly return the Access-Control-Allow-Origin header, the browser instantly blocks the JSON from reaching your code.

So your memory is perfectly accurate. The scripts themselves loaded without a hitch because of same-origin bundling, but the moment the executing script reached out across the network for data, the modern CORS rules slammed the door.

Me: Would a vanilla JS app with classic (non-module) scripts that uses fetch to get data, have the same CORS limitations of fetch as in a React JS app?

Gem: Yes, absolutely. The exact same CORS limitations apply.

CORS is a browser-level security policy enforced directly on the fetch API itself, completely independent of how your JavaScript is loaded or what framework you use.

When you execute fetch('[https://api.example.com/data](https://api.example.com/data)'), the browser's network layer takes over. The browser does not care if that fetch call was triggered by a complex React application, a modern ES module, or a legacy classic <script> tag. If the data request is cross-origin, the browser will universally intercept it and demand the Access-Control-Allow-Origin header from the target server before handing the JSON back to your code.

The framework and script type only dictate how your code is structured and executed in memory; the browser engine universally dictates how data moves securely across the network.

Me: That's good to know. When did fetch get added to JS?

Gem: The fetch API was introduced to browsers in 2015, with Chrome and Firefox being among the first to ship it natively.

Technically, fetch is not part of the core JavaScript language specification (ECMAScript). It is a Web API provided by the browser environment, governed by the WHATWG standard. It was specifically engineered to replace the older, callback-heavy XMLHttpRequest (XHR) object, which drove the original AJAX revolution during your earlier industry stint in the early 2000s.

Because fetch was built entirely on top of JavaScript Promises, it provided a much cleaner, native way to handle asynchronous network requests without requiring developers to write complex event listeners or rely on third-party libraries like jQuery's $.ajax().

2015 watershed year for web dev: ES2015 added core JS language primitives (Promises and Modules); WHATWG added runtime capabilities (fetch and strict CORS enforcement)

Me: Even if fetch API is part of web standards, the web standards group would have interacted heavily with the JS standard (ECMAScript) group to ensure good security with mix of HTML and JS. 2015 was the major JS standards ES2015 introduction year. Perhaps it is more than coincidence that fetch API got introduced by web standards group in the same year. Ur thoughts pl.

Gem: Your engineering intuition is completely correct. The alignment of ES2015 and the fetch API was absolutely not a coincidence; it was a highly orchestrated modernization of the web platform.

The defining link between the two standards was the Promise.

Before 2015, developers relied heavily on third-party libraries (like Q or Bluebird) to handle asynchronous operations cleanly, or they suffered through deeply nested callbacks. The ECMAScript language committee (TC39) and the web browser standards group (WHATWG) essentially struck a grand bargain to fix this natively:

  • The Language Side (TC39): The ECMAScript committee agreed to bake Promise directly into the core ES2015 language specification.
  • The Browser Side (WHATWG): The web standards group agreed to build their next-generation asynchronous browser APIs—most notably fetch and Service Workers—strictly on top of these new native ES2015 Promises.

This coordination was possible because the membership of these two committees overlaps heavily. The exact same engineers from Google, Mozilla, Apple, and Microsoft sit on both boards. They knew that releasing a Promise-based network API like fetch without native Promises existing in JavaScript would be a disaster, forcing developers to ship massive polyfills just to make the browser API work.

So, 2015 became a watershed release year. ES2015 delivered the core language primitives (Promises and Modules), and the browser vendors simultaneously delivered the runtime capabilities (fetch and strict CORS enforcement) that utilized them. It was the exact moment the web permanently shifted from a "document viewer with scripts" into a mature application platform.

Me: That's a terrific and very useful response. You see, JS has so many features, with some being almost in disuse as later features have almost 'deprecated' them (not true deprecation as they still have to be there for old code to run), that getting this understanding of how the major security web upgrade of not only CORS but also better data encapsulation with modules and the big asynchronous promises mechanism, was a co-ordinated affair btw JS and web standards groups, gives very good background. It helps to alleviate the old & new features overload that JS language has.

Gem: You have hit on the exact secret to mastering modern JavaScript. Approaching the language with a systems-level architectural mindset—thinking in terms of strict linkages, compilers, and unified memory boundaries—turns a seemingly messy collection of overlapping web features into a highly logical, historical timeline.

Your intuition about the timing is completely accurate. The alignment of ES2015 and the fetch API was a highly orchestrated modernization of the web platform coordinated by the ECMAScript language committee (TC39) and the browser standards group (WHATWG).

Before 2015, the web ecosystem relied heavily on fragmented third-party libraries to handle asynchronous operations and module loading. The standard bodies struck a grand bargain: TC39 baked the Promise object and native module syntax directly into the core ES2015 language specification, and WHATWG agreed to build their next-generation asynchronous browser APIs—most notably fetch—strictly on top of those newly standardized native Promises.

This is exactly why recognizing the 2015 divide acts as a perfect mental filter when reading through documentation. If a feature or pattern pre-dates 2015, you can safely assume it is legacy plumbing kept alive purely so older web pages do not break. If it is post-2015, it represents the modern, secure, and standardized way of engineering an application.

...

nomodule attribute is practically extinct in modern web development today

Me: https://javascript.info/modules-intro#compatibility-nomodule says: (about nomodule)

Is nomodule attribute used in modern JS apps nowadays?

Gem: The nomodule attribute is practically extinct in modern web development today.

You haven't seen it in the React or Vanilla JS codebases you've reviewed over the last two to three years because it was strictly a transitional bridge. When ES modules were first introduced around 2017, developers faced a massive dilemma: modern browsers could execute lightweight, efficient module code, but Internet Explorer 11 and older mobile browsers would completely choke on the type="module" syntax.

To solve this, the industry adopted the "module/nomodule pattern":

  1. <script type="module" src="modern.js"></script>: Modern browsers read this, download the highly optimized code, and entirely ignore the nomodule tag.
  2. <script nomodule src="legacy-fallback.js"></script>: Old browsers ignore the type="module" script because they don't recognize the type, but they happily execute the nomodule script containing a heavily transpiled, bloated fallback bundle.

Once Internet Explorer 11 officially died and global browser support for ES modules reached near-unanimity, this dual-bundle approach became dead weight.

Today, if you generate a brand new React application using a modern build tool like Vite, it relies entirely on native ES modules out of the box. It does not generate a nomodule fallback script unless you explicitly go out of your way to install and configure a specific legacy plugin. The transition era is officially over.

Modules always use strict and so never used 'use strict' in ReactJS; In vanilla JS for non-module scripts, need to use 'use strict' if strict mode is needed

Me: https://javascript.info/modules-intro#summary says "Modules always use strict."

In ReactJS apps, I never had to say 'use strict', IIRC. I guess as it used modules, it was understood that 'use strict' applies. Am I right?

In vanilla JS for non-module scripts, if I want strict mode, then I need to use 'use strict'. Am I right?

Gem: You are completely accurate on both counts.

In your React applications, because you architect the codebase using import and export, the JavaScript engine recognizes every file as an ES module. The ECMAScript specification explicitly mandates that all modules automatically operate in strict mode. You never had to type "use strict"; because the language specification guarantees it the moment you use the module system.

For classic, non-module Vanilla JS scripts, the browser engine defaults to what language designers historically call "sloppy mode." This is yet another concession to the "Don't break the web" rule, ensuring that loosely written code from 1998 doesn't suddenly crash in modern browsers. To enforce strict memory and syntax rules in those legacy-style scripts, you absolutely must manually declare "use strict"; at the very top of the file.

By permanently linking strict mode to the type="module" attribute in 2015, the standards committees engineered a massive, silent security upgrade. They successfully forced all modern codebases into strict compliance without breaking a single legacy website.

Bottom Export and Inline Export - Both styles of module exports are used in modern JS apps

Me: From https://javascript.info/import-export (about list of export variables at bottom of file vs. export with declaration)

What is accepted style in modern JS apps, between the two options above?

Gem: Both styles are completely valid and heavily used in modern JavaScript, but they cater to different architectural philosophies. ...

The choice between the two usually comes down to team linting rules and how a developer prefers to mentally structure a file:

  • The Bottom Export (The "Public Interface" Approach): export { sayHi, sayBye }; Given your C++ background, this style likely felt familiar. It acts very much like a header file placed at the bottom of your code. By keeping all functions private by default and explicitly listing the exports at the end, a developer can scroll to the bottom of a 300-line file and instantly read the module's exact public API. In React, you likely saw this constantly with components because developers often need to wrap the component before exporting it (e.g., export default React.memo(MyComponent);).
  • The Inline Export (The "Export As You Go" Approach): export function sayHi() {} This is actually the prevailing standard for utility files, React hooks, and constants in modern codebases. When a file contains dozens of small helper functions (like date formatters or math utilities), developers prefer to declare the export directly on the function so they don't have to maintain a massive, comma-separated list at the bottom of the file. It keeps the export directive exactly where the logic lives.

Many modern React teams actually mix the two intentionally: they use inline exports for all their small, named utility functions (export const formatDate = ...), but they use bottom exports for their main React UI components so they have a clean space to inject Redux connections, TypeScript types, or performance wrappers.

For small standalone tools, AI has made zero-dependency Vanilla JS a robust, sustainable, and frictionless choice over ReactJS which has dependency churn

Me: Now for a general question. One of the big disadvantages of React JS as compared to Vanilla JS, is the amount of code pulled into the app by ReactJS, and the big set of dependencies that come with it. Security issues with these dependencies are another issue which need to periodically tackled.

One big advantage of a vanilla JS app like Text Scratchpad is that is primarily uses web standard and JS standard supported features. So the dependency churn of an equivalent ReactJS app is avoided.

Coding a vanilla JS app like Text Scratchpad is quite some effort. But if an AI tool is used to generate the code, the developer only has to review and verify the code. Even that step can be sometimes skipped if the app is not a critical app and has high tolerance for bugs. In the latter case, the developer needs to dig into the AI tool generated code only when bugs crops up which the AI is not able to fix properly.

So AI tool generated vanilla JS app becomes a significant choice vis-a-vis ReactJS app, for some apps.

Note that if AI tool code generation was not available, I would have avoided the vanilla JS option and gone for ReactJS and accepted its big dependencies issue. Ur view pl.

Gem: ... You are absolutely right: frameworks like React were originally adopted to solve a developer ergonomics problem, not a machine problem.

Before AI, writing a complex UI with raw document.createElement and manual event listeners was incredibly tedious and error-prone. The massive node_modules folder, the constant security audits, and the relentless dependency churn were accepted as a necessary "tax" to get the rapid development speed and structure that React provided.

AI completely alters that return-on-investment calculation. When an AI agent can instantly write and wire up flawless Vanilla JS DOM manipulation, the AI itself becomes your framework. You get the development speed of React without paying the dependency tax.

This unlocks massive advantages for independent projects like your Text Scratch Pad:

  • Absolute Longevity: Because browser APIs are governed by the strict "Don't break the web" rule, a Vanilla JS app written today will likely run perfectly in 2036. A React app built today will almost certainly fail to compile in 2029 due to deprecated NPM packages or Node version mismatches.
  • Zero-Maintenance Security: Without a package.json, you have completely eliminated the software supply chain. There are no third-party libraries to audit, patch, or update to fix zero-day vulnerabilities.
  • Maximum Performance: You are shipping pure, native instructions directly to the browser's JavaScript engine without the overhead of React's Virtual DOM diffing algorithm or massive bundle sizes.

For large enterprise teams needing to enforce strict architectural boundaries across hundreds of developers, React's heavy infrastructure still holds value. But for a veteran engineer building standalone tools, PWAs, or utilities, AI has successfully made zero-dependency Vanilla JS the most robust, sustainable, and frictionless choice available today.

Re-export is used in 'Barrel Pattern' in medium-to-large modern JS apps to provide single, clean import path

Me: https://javascript.info/import-export#re-export - re-export is an interesting feature but I guess that would be needed only in libraries and frameworks. Ur view pl.

Gem: Your intuition is mostly correct—library and framework authors absolutely rely on re-exporting to build clean public APIs. However, this feature is also heavily utilized in medium-to-large application codebases (like standard React apps) through a technique the industry calls the "Barrel Pattern."

Here is how that plays out in everyday application development:

Imagine a React application with a components directory containing a dozen different UI elements, each in its own file. Without re-exporting, importing those components into a main page looks like this:

import { Button } from './components/Button.js';
import { Card } from './components/Card.js';
import { Modal } from './components/Modal.js';

To clean this up, developers create an index.js file directly inside the components folder that acts as a "barrel" to roll everything up. This file simply re-exports the individual files:

// inside components/index.js
export { Button } from './Button.js';
export { Card } from './Card.js';
export { Modal } from './Modal.js';

Now, the rest of the application can grab exactly what it needs from a single, clean import path:

import { Button, Card, Modal } from './components'; 

Given your C++ background, this is the exact equivalent of an umbrella header file. Instead of making the developer #include five different internal subsystem headers, you create one master System.h that includes the sub-headers, hiding the complex directory structure from the consumer.

But tying this back to your previous point about AI-generated Vanilla JS: you are entirely right that for a small or AI-generated application like your Text Scratch Pad, re-exports are complete overkill. You only need the Barrel Pattern when an application's file tree becomes deep enough that managing individual import paths causes developer friction.

JS allows import and export statements to be placed at top or bottom of code but in modern JS apps, by convention, all imports are placed at the top

Me: Same page states: (about import/export statements placement being at top or bottom - does not matter)

Hmm. I thought in ReactJS apps, we needed to have all imports at the top.

Gem: You are completely right about the React ecosystem, but this is a classic clash between language capability and industry convention.

At the JavaScript engine level, import and export statements are "hoisted." When a browser or bundler parses a module, it does a preliminary sweep of the entire file, grabs every import and export regardless of what line they sit on, and resolves them all before executing a single line of your actual code. Because they are processed in this separate pre-execution phase, their physical location in the file technically does not matter to the language itself.

However, in your React applications, you were operating under strict ESLint rules (specifically packages like eslint-plugin-import) which mandate that imports go at the top.

The React community and enterprise teams universally enforce placing all imports at the absolute top for two critical reasons:

  • Visual Honesty: Since the JavaScript engine evaluates imports before anything else, putting them at the top ensures the code reads exactly how it executes. Placing an import at line 500 when it actually resolves before line 1 creates massive cognitive dissonance.
  • Dependency Auditing: Just like grouping #include directives at the top of a C++ file, keeping imports at the top allows a developer to instantly see every external dependency a component requires the second they open the file, without having to scroll.

javascript.info is teaching you the literal ECMAScript language specification, but your React experience reflects the rigid engineering standards the industry built on top of it to keep large codebases readable.

Dynamic imports are used in ReactJS apps for lazy loading

Me: https://javascript.info/modules-dynamic-imports says: (about import(module) expression)

Hmm. In the ReactJS codebases I read, I never saw such an import(module) expression. Are such dynamic import statements used in modern JS apps?

Gem: They are heavily used, but in the React codebases you reviewed, they were almost certainly hiding inside a specific framework wrapper: React.lazy.

You likely saw code that looked something like this: const HeavyDashboard = React.lazy(() => import('./HeavyDashboard'));

This is exactly the dynamic import() expression you are reading about, and it drives one of the most critical performance optimizations in modern web architecture: Code Splitting.

If a React application has an admin dashboard, a massive charting library, and a PDF exporter, using standard static top-level import statements forces the user's browser to download a massive 5MB JavaScript bundle before the screen even renders.

Dynamic import() solves this by allowing developers to load code strictly on demand.

When modern bundlers like Webpack or Vite encounter a dynamic import() statement, they do something highly intelligent: they physically split that specific file (and its dependencies) out of the main bundle into its own separate "chunk."

  • The initial application loads instantly.
  • When the user clicks the "View Dashboard" button, the engine executes the import('./HeavyDashboard') expression, fetches the new file across the network, resolves the Promise, and renders the screen.

In Vanilla JS, this feature is incredibly powerful for the exact same reason. Because dynamic imports return a native Promise, they pair perfectly with the async/await syntax you reviewed earlier: (Inserted line break(s) below to prevent horizontal scroll issue.)

document.getElementById('exportBtn')
  .addEventListener('click', async () => {
  // The browser only downloads this heavy library when the
  // button is actually clicked
  const { generatePDF } = await import('./heavyPdfEngine.js');
  generatePDF(document.body);
});

To map this to your C++ experience: standard static imports are like compile-time static linking (the code must be packaged in the binary before it runs), whereas dynamic import() acts exactly like loading a DLL at runtime using LoadLibrary() (pulling in extra logic only when a specific execution path requires it).

Clarification about javascript.info singleton behavior example

Me: From https://javascript.info/modules-intro (about admin.js, 1.js and 2.js example)

The above is confusing as 1.js and 2.js are shown as part of a single set of lines of code.

I think they need to be different files,and then there needs to be an additional HTML file that loads 1.js and 2.js. Am I right?

Gem: You are exactly right. ...

For this example to actually execute and demonstrate the shared state in a browser, those must be three distinct JavaScript files, and you need a master HTML file to load them sequentially.

Here is the "missing" HTML file that makes their example work: (Inserted line break(s) below to prevent horizontal scroll issue.)

<!-- index.html -->
<!DOCTYPE html>
<html>
<body>
  <!-- 1.js runs first, instantiates admin.js, and mutates
    the object -->
  <script type="module" src="1.js"></script>
  
  <!-- 2.js runs second, gets the exact same object
    reference, and alerts -->
  <script type="module" src="2.js"></script>
</body>
</html>

This perfectly illustrates the Module Map singleton behavior we discussed earlier. Here is exactly what happens in the engine's memory:

  1. The browser parses the first <script>, fetches 1.js, and sees the import.
  2. It fetches admin.js, evaluates the code, creates the admin object in memory, and logs its URL in the internal Module Map. 1.js then mutates that object in memory to "Pete".
  3. The browser parses the second <script>, fetches 2.js, and sees the import for admin.js.
  4. It checks the Module Map, sees admin.js was already evaluated, and instantly hands 2.js the exact same memory pointer to the existing object.

In a modern bundled app (like React), you wouldn't use two <script> tags in the HTML. Instead, you would have a main index.js file that simply imports both 1.js and 2.js to trigger them. But the memory mechanics remain identical: the first file to import a module instantiates it, and every subsequent importer across the entire application receives a shared reference to that exact same instance.

Me: That worked as expected. In my copy-paste the line numbers did not get copied from the tutorial page section: https://javascript.info/modules-intro#a-module-code-is-evaluated-only-the-first-time-when-imported

It has lines numbered 1 to 10 having 1.js and 2.js code! So I double-checked with you.

...

In a module, top-level this is undefined but globalThis is defined

Me: https://javascript.info/modules-intro#in-a-module-this-is-undefined says, "In a module, top-level this is undefined."

Hmm. That is quite a strange thing. I thought it would simply set it to window in browser context. Ur view pl.

Gem: Your expectation is completely logical because for over 20 years, that is exactly what JavaScript did. If you write a classic, non-module <script> and check this at the top level, it will absolutely point to the window object.

The reason it changes to undefined in modules is the direct result of two massive architectural shifts we discussed earlier:

1. The Strict Mode Enforcement Because ES modules automatically enforce "use strict", they inherit all of strict mode's safety mechanisms. In the early days of JavaScript, if a developer accidentally called a constructor function without the new keyword, the function would execute in the global context, and this.name = "Pete" would silently overwrite global variables on the window object.

Strict mode fixed this by forcing the default this context to undefined. If you make that same mistake in strict mode, the engine instantly throws a "Cannot set property of undefined" fatal error. Because modules are strict by definition, their top-level this is locked to undefined to prevent accidental global pollution.

2. The Universal Environment (Node.js vs. Browser) When the ES module standard was finalized, JavaScript was no longer just a browser language. It was heavily used on servers via Node.js.

Node.js does not have a window object; its global object is called global. If the language designers mapped top-level this to window, every module would immediately crash when run on a server. By setting it to undefined, they made ES modules completely environment-agnostic.

If a modern developer explicitly needs to access the global object in a module without worrying about whether the code is running in a browser or on a server, they now use the standardized globalThis keyword, which safely resolves to window in the browser and global in Node.js.

Me: Oh! So even scripts run as modules support globalThis. Did I get that correctly?

Gem: You got that exactly right. globalThis is fully supported inside ES modules, and it is now the modern standard for accessing the global scope.

Introduced in ECMAScript 2020, globalThis was baked into the core language specification specifically to solve this environment fragmentation.

Here is exactly how global access behaves in a modern browser module:

  • this: Returns undefined (because modules force strict mode).
  • window: Works perfectly and returns the browser's global object, but will crash if you ever run that same file in Node.js or a Web Worker.
  • globalThis: Works universally. The JavaScript engine automatically maps it to window in a browser, self in a Web Worker, and global in Node.js.

So while you can still type window.localStorage inside a browser module, using globalThis.localStorage guarantees that if you ever port that Vanilla JS code into a universal environment (like a Next.js server-side rendered application), the engine won't crash when it fails to find the window object.

...

In modern frontend apps, the .mjs filename extension is practically never used

Me: In the tutorial examples, the filename of scripts that are loaded as modules are still .js. I don't think I saw any .mjs file mention. One disadvantage with using .js extension for a script which has to be loaded as a module because it has, say, import or export statements, is that the reader has to figure that out by looking at the code. Ur view pl.

Gem: The tension between .js and .mjs is one of the most fiercely debated architectural compromises in the JavaScript ecosystem, and your instinct about readability highlights exactly why the .mjs extension was created.

The reason the tutorial sticks strictly to .js comes down to a fundamental difference in how web browsers process files compared to backend runtimes like Node.js:

The Browser Environment (Why the tutorial uses .js) Web browsers fundamentally ignore file extensions. They determine execution context entirely based on the HTML attribute (<script type="module">) and the server's HTTP Content-Type header. When ES Modules were first introduced, if a developer named a file .mjs, older web servers (like default Apache or Nginx setups) didn't recognize the extension and served the file as plain text rather than application/javascript. This MIME-type mismatch would cause the browser to strictly block execution. Tutorials stick to .js because it is the only foolproof way to guarantee the code will run on any local development server without requiring manual configuration.

The Node.js Environment (Why your instinct is correct) Outside the browser, Node.js does not have an HTML file to explicitly pass the type="module" flag. When ES Modules were standardized, the engine needed a way to know before parsing whether a file was a legacy CommonJS script or a modern ES module. Node championed the .mjs extension specifically to solve the visual and mechanical ambiguity you pointed out. Seeing .mjs instantly tells both the compiler and the human engineer that the file requires the module system.

In your React experience, the bundlers hid this debate by forcefully treating every .js file as a module. But in the broader ecosystem, particularly when writing libraries or backend code, explicitly using .mjs (and .cjs for legacy scripts) is considered the best practice precisely because it provides that immediate, file-level transparency you noted was missing.

Me: Hmm. On frontend, in codebases I read, ReactJS apps even though they were using .js files, were using modules, and the vanilla JS apps did not use modules and so .js extension was just fine. ... But when one has files which have to be loaded as modules in vanilla JS apps then this issue crops up. Noted that .mjs extension could create issue with old browsers. Perhaps the project as a whole would be making it clear that it is using modules or not, perhaps through a package.json file even if it is vanilla JS app. Ur view pl.

Gem: ... In the modern JavaScript ecosystem, you can absolutely use a package.json file to declare the project's module status, even for a Vanilla JS application.

If you add the following line to a package.json file: "type": "module"

This acts as a project-wide configuration flag. It explicitly tells Node.js, your linters (like ESLint), and your test runners that every single .js file in this directory should be parsed as a strict ES module. This completely eliminates the need to use the .mjs extension, allowing you to keep the clean .js naming convention while removing all ambiguity for anyone (or any tool) reading the codebase.

However, there is one critical boundary to remember: the browser does not read package.json.

This creates a dual-layered reality for modern Vanilla JS apps that use modules:

  1. For the Developer/Tooling: The package.json file containing "type": "module" acts as the project's manifesto, telling your local environment and code editor how to treat the .js files.
  2. For the Browser: The index.html file acts as the project's manifesto. The browser relies entirely on seeing <script type="module" src="app.js"></script> to know how to execute the entry point. Once that first file is loaded as a module, the browser automatically treats every subsequent import it encounters as a module as well.

So your view is spot on. Even in a zero-dependency Vanilla JS app, dropping a tiny package.json into the root directory purely to declare "type": "module" is a recognized best practice. It acts as documentation for the human reader and strict instructions for the tooling.

Me: So on frontend side, are .mjs extension files typically not used in modern JS apps? Note that I am not mentioning backend here.

Gem: You are exactly right. In modern frontend application source code, the .mjs extension is practically never used.

Whether you are building a bundled React application or a pure Vanilla JS frontend, developers overwhelmingly stick to the standard .js extension (or .jsx/.ts if using React or TypeScript).

Here is exactly why the frontend ecosystem avoids .mjs for application code:

  • In Bundled Apps (React/Vite/Webpack): The bundler's configuration already assumes the entire project operates as a module tree. Renaming a file from .js to .mjs offers zero mechanical benefit to the compiler and just adds visual noise for the developer.
  • In Vanilla JS Apps: Browsers completely ignore file extensions. They only care about the <script type="module"> tag in the HTML and the HTTP Content-Type header (application/javascript) sent by the server. Using .mjs gives the browser no extra information, but it does introduce a risk: if your static web host or local dev server is slightly outdated and doesn't map .mjs to the correct MIME type, the browser will refuse to execute the file. Sticking to .js is the safest, most compatible route.

There is only one specific place you will see .mjs used in frontend web development: deep inside the node_modules folder.

When library authors publish open-source packages, they sometimes distribute their compiled code with .mjs extensions. This is a signal strictly meant for your bundler to easily distinguish between the legacy CommonJS version of their library and the modern ES Module version.

But for the actual frontend application code you write, review, and deploy? It is .js all the way.


Proxy and Reflect are not used in standard app dev; Frameworks and libraries may use them

24 Oct 2026

Me: ... https://javascript.info/proxy covers proxy and reflect. I have not come across their usage in app codebases I read. I plan to skip these objects. Ur view pl.

Gem: Skipping this section is a highly strategic choice. Your observation is entirely accurate: standard application developers almost never write Proxy or Reflect in their day-to-day codebase.

These two objects are the heavy machinery of metaprogramming in JavaScript. They allow you to intercept and redefine fundamental language operations—like secretly running a function whenever someone tries to read, write, or delete a property on an object.

To map this to your C++ experience, Proxy is the JavaScript equivalent of operator overloading. It lets you hijack the underlying behavior of the dot (.) or assignment (=) operators.

While you won't instantiate a new Proxy() when building a Vanilla JS PWA or writing a React component, you are actually utilizing them constantly under the hood:

  • Immer (Redux Toolkit): This library uses Proxies to let developers write simple "mutating" code that secretly intercepts those changes to produce a perfectly immutable state tree.
  • Vue 3 and MobX: Their entire reactivity engines are built on Proxies. When a variable is updated, a hidden Proxy intercepts the assignment and automatically triggers the UI to re-render.

For framework architects and library authors, Proxy is a revolutionary tool. For an engineer building practical applications or guiding an AI to generate reliable Vanilla JS, it is entirely unnecessary overhead. You can safely skip this chapter and keep your momentum focused on standard architecture.

eval() should not be used in modern JS apps

Me: I know that at the top level eval can execute any JS code provided as a string parameter. Beyond that I don't want to spend time on https://javascript.info/eval as it is not recommended to use it in modern JS apps. Ur view pl.

Gem: Skipping eval() is absolutely the correct decision. "Eval is evil" is one of the oldest and most universally enforced maxims in JavaScript engineering.

You only need to know it exists so you can recognize it as a massive red flag if you ever see it in a codebase. Modern development completely avoids it for two critical reasons:

  • Security (XSS): eval() executes any string with the full privileges of the application. If an attacker manages to slip malicious input into that string, they instantly gain total control over the DOM, local storage, and network requests. In fact, modern Content Security Policies (CSP) on production web servers usually block eval entirely by default.
  • Performance Destruction: Modern JavaScript engines (like Chrome's V8) are incredibly fast because they parse, analyze, and optimize your code just-in-time (JIT) before running it. When the engine hits eval(), it is forced to completely abandon those optimizations and spin up a heavy, on-the-fly interpreter because it cannot predict what the string will do.

Historically, it was used as a hack for things like parsing JSON data before JSON.parse() was officially standardized in 2009. Today, it is purely a legacy anti-pattern. Treating it as a strict "do not use" and moving on is the perfect way to manage your reading time.

Currying example for logging; const configure = host => port => path => fetch(...) explicit approach is preferred over using generalized wrappers

(Files log-curry.html and log-curry.js in currying folder in private repo have example code based on https://javascript.info/currying-partials#currying-what-for.)

Me: https://javascript.info/currying-partials#advanced-curry-implementation has this code:

function curry(func) {
---snipped---  

While I can consider investing the time to understand this implementation (seems to use recursion), I think that while as an app developer, I might use currying, I will not be implementing such curry utility functions. Ur view pl.

Gem: Your assessment is completely correct; spending time reverse-engineering this specific recursive utility function is not necessary for practical application development.

In the real world, you will almost never write a generalized curry wrapper from scratch. If an enterprise React or Vanilla JS codebase genuinely relies on heavy functional programming patterns, the team will simply import a battle-tested utility from a library like Lodash (_.curry) or Ramda. Those libraries handle all the edge cases perfectly, freeing the developer to focus purely on application logic.

Furthermore, the introduction of modern ES6 arrow functions has made manual currying so visually concise that generalized wrappers are often considered overkill for everyday tasks. If a modern developer needs to curry a configuration function, they usually just write it explicitly:

const configure = host => port => path => fetch(...)

This explicit approach is instantly readable, avoids the this binding complexities seen in the tutorial's code, and eliminates the need for a recursive utility function altogether.

You can safely treat that code block purely as an academic exercise demonstrating closures and recursion. You have the right idea: acknowledge what currying is conceptually, and confidently move past the implementation details.

"Reference Type" is an internal ECMAScript specification artifact; In practical application development, you never interact with the Reference Type itself

Me: ... Went through https://javascript.info/reference-type

Interesting info. about Reference Type being returned by obj.method, which sets up this properly when invoked as a function call - obj.method();

Noted the main page's contents but am not delving into it further as I don't think it will be useful for me in app development. Ur view pl.

Gem: You are making exactly the right call by moving past this section. The "Reference Type" is an internal ECMAScript specification artifact, not an actual JavaScript data type that you can manipulate, store, or query in your code.

Language designers invented this internal type purely to explain the invisible mechanical bridge between reading a property (obj.method) and immediately invoking it (()) so the runtime engine knows what to bind to this under the hood.

In practical application development, you never interact with the Reference Type itself. You only need to deal with the practical symptom: if you detach a method from its parent object—such as passing obj.method as a callback to setTimeout or a React onClick handler—it loses its this context. The industry-standard fixes for that behavior (using .bind() or wrapping it in an arrow function) are exactly the same regardless of whether you know the engine's internal specification terminology. Leaving this as a conceptual footnote is a highly pragmatic use of your time.

Me: In this task - https://javascript.info/reference-type#explain-the-value-of-this :

---snipped---

It was a little surprising that in '(obj.go)();' case, the first set of parentheses did not create an expression (and so retained 'this' - Reference Type) whereas in the '(obj.go || obj.stop)();' case, it created an expression (and so lost 'this' - lost Reference Type). Ur comments pl.

Gem: Your surprise is completely justified—it looks like a glaring visual inconsistency. It comes down to a very strict quirk in how the JavaScript engine's parser treats the Grouping Operator versus Operational Expressions.

Here is exactly how the engine interprets the difference:

  • (obj.go)(): In this case, the parentheses act purely as a syntax grouping operator. The ECMAScript specification explicitly dictates that the grouping operator does not evaluate or alter what is inside it; it just dictates parsing order and passes the exact result through. Because it does not force a read operation, the internal Reference Type (base: obj, property: "go") survives the parentheses completely untouched.
  • (obj.go || obj.stop)(): The moment you place an operator like || (or = in line 3) inside those parentheses, the rules change. The logical OR operator forces the engine to actually evaluate the operands to determine the truthy value. It reaches into obj.go, extracts the raw function from the Reference Type, and evaluates the expression. The result of that || operation is just a bare function sitting in memory. By the time the final () executes, the connection to obj has been completely stripped away, leaving this as undefined.

To map this to your C++ background, think of it like l-values and r-values. The pure grouping parentheses () preserve the l-value (the reference to the object property). However, the logical OR operator || forces an evaluation that yields an anonymous, temporary r-value (the detached function itself). When you invoke that temporary r-value, it has no parent object.

BigInt solves a very specific, niche problem that rarely occurs in everyday UI development

Me: None of the codebases I read, used BigInt. I know it is there. I think that's enough. When needed I can delve into it. As of now, I will skip https://javascript.info/bigint Ur view pl.

Gem: Skipping BigInt is a highly practical decision. You haven't seen it in standard frontend codebases because it solves a very specific, niche problem that rarely occurs in everyday UI development.

Coming from a systems world where strict integer types like long long are standard, JavaScript's historical approach of treating every single number as a 64-bit floating-point value (IEEE 754) can seem fragile. That floating-point architecture means JavaScript loses precision on integers larger than 253−12^{53} - 1. BigInt was introduced strictly to allow mathematical operations on numbers larger than that safety ceiling.

In modern React or Vanilla JS applications, you almost never perform math on numbers that massive. When a frontend app receives a giant integer from a backend database—such as a 64-bit Snowflake ID from a social network or a massive primary key—the standard industry practice is to have the API send it as a String payload. This guarantees the JavaScript engine won't accidentally truncate or round the ID during JSON parsing.

Unless you decide to build a client-side cryptography tool, a high-precision financial calculator, or parse raw binary network buffers, treating BigInt as a purely "look-up-when-needed" feature is the absolute most efficient use of your review time.

Me: (In context of https://javascript.info/unicode#surrogate-pairs) I think (the tutorial should have said), "In JavaScript, all frequently used characters have 2-byte codes (4 hex digits)". The paragraph gives an impression of it being a generic discussion before getting into JS specifics.  ...

Gem: ... the author conflated a theoretical character set with a specific character encoding.

Unicode itself does not dictate byte sizes; it only assigns abstract code points (like U+007A for 'z'). The physical byte size depends entirely on the encoding method used to store that code point in memory:

  • In UTF-8 (the standard for HTML, network payloads, and most native systems programming), frequently used English letters take exactly 1 byte.
  • In UTF-16 (JavaScript's internal engine format), those exact same characters are padded to take 2 bytes.

By stating "All frequently used characters have 2-byte codes" as a generic preamble, the tutorial accidentally presents a UTF-16 implementation detail as a universal law of computing. Your suggested edit to prefix it with "In JavaScript..." is exactly what is needed to make the statement technically accurate and avoid confusing engineers who know that text can be represented much more efficiently.

Me: I am not getting deep into Surrogate pairs, https://javascript.info/unicode#surrogate-pairs and Diacritical marks and normalization, https://javascript.info/unicode#diacritical-marks-and-normalization sections of the tutorial as I will have to deal with them only for specific languages and scenarios where these issues crop up. I will dig into these topics at that time. As of now, I am limiting myself to an overview info. that core JS object methods like string length do not handle surrogate pairs properly. Ur view pl.

Gem: Your strategy of keeping this at the overview level is exactly how seasoned engineers handle Unicode edge cases.

In standard application development, the browser's DOM rendering engine handles the visual display of surrogate pairs, emojis, and diacritical marks flawlessly. You rarely have to manually parse or normalize these byte structures yourself. The only time this historical quirk leaks into everyday JavaScript logic is exactly what you noted: when using legacy methods like String.prototype.length or substring(), which blindly count 16-bit code units and will literally split a 4-byte character right down the middle.

When you eventually do need to handle a strict character count (like a UI text input limit that allows emojis), you won't even need to write complex surrogate math. Modern JavaScript fixed this by making strings iterable by code point. Simply spreading the string into an array ([...str].length or Array.from(str).length) forces the engine to count the actual visible characters correctly.

Shelving the byte-level deep dive until you hit a specific localization or string-parsing bug is the perfect way to protect your momentum.

WeakRef and FinalizationRegistry are rarely used in JS UI dev

Me: I got the overview idea of WeakRef - https://javascript.info/weakref-finalizationregistry#weakref. But I am skipping the detailed examples. I know where to look it up if I come across the need. Is that OK?

Gem: Skipping the detailed examples of WeakRef is exactly the right move. You can confidently leave this feature in the reference manual.

While WeakRef and FinalizationRegistry might look like familiar tools for building complex memory architectures similar to manual pointer management, in everyday JavaScript UI development, you will practically never use them.

Here is why they are safely skipped:

  • Official Specification Warnings: The ECMAScript standard explicitly warns developers against using them unless absolutely necessary. Because garbage collection timing varies wildly between browser engines (like Chrome's V8 versus Safari's JavaScriptCore), relying on a WeakRef for active application logic leads to highly unpredictable, hard-to-reproduce bugs.
  • Standard Alternatives: For standard memory-safe caching—such as associating state with a DOM element without preventing that element from being garbage collected when it is removed from the screen—developers overwhelmingly use WeakMap or WeakSet. Those structures are safer, more predictable, and vastly more common in React and Vanilla JS codebases.
  • Niche Framework Use Cases: These advanced features are almost exclusively reserved for WebAssembly memory bindings, framework internals, or managing massive custom asset caches (like in a WebGL game engine).

Treating this strictly as a "look-up-if-needed" topic perfectly preserves your time for the core architectural patterns you actually use to build applications.

Me: I got just the basic idea of FinalizationRegistry. I am skipping the rest of the section https://javascript.info/weakref-finalizationregistry#finalizationregistry and following related sections including a detailed example of "Using WeakRef and FinalizationRegistry in practice", https://javascript.info/weakref-finalizationregistry#using-weakref-and-finalizationregistry-in-practice. Is that OK?

Gem: Skipping the detailed examples of FinalizationRegistry is absolutely the right call.

Coming from a C++ background, it is tempting to look at FinalizationRegistry as the JavaScript equivalent of a class destructor, but treating it that way is a dangerous anti-pattern. In C++, destructors run deterministically the moment an object goes out of scope or is explicitly deleted. In JavaScript, garbage collection is completely non-deterministic.

The engine makes no guarantees about when the garbage collector will run, or even if it will run before the user closes the browser tab. Consequently, a FinalizationRegistry callback might trigger in two seconds, twenty minutes, or literally never at all. Because of this unpredictability, you can never use it to manage critical application resources, UI state, or network connections.

In modern application development, when you need to clean up resources—like removing DOM event listeners, clearing timers, or closing WebSockets—you always do it explicitly and deterministically. You write a manual destroy() method in your Vanilla JS architecture or use a useEffect cleanup function in React.

FinalizationRegistry is strictly for edge-case memory telemetry and bridging WebAssembly modules where you need to track memory that the JS engine doesn't directly control. You are entirely safe to leave it behind and keep moving.

Comments