In HTML, how does <script type="module" src="app.js"></script> behave with respect to the parser compared with a classic <script src="app.js"></script>, and what effect do the defer and async attributes have on a module script?
answer
- the default flips to deferred
- one attribute becomes a no-op
- the other still works, even inline
- cross-origin needs a header now
- the legacy twin attribute on classic tags
basics
~20 sA module script is deferred by default: it never blocks the parser and runs after parsing, in document order. The defer attribute is therefore redundant, while async still works and makes it run as soon as its graph is ready — including on inline module scripts.
solid answer
~40 s`type="module"` changes the default loading mode. Unlike a classic script, a module script never blocks the HTML parser: the browser fetches it and its dependency graph in parallel with parsing and evaluates it after parsing completes, in document order relative to other deferred and module scripts. That makes `defer` a no-op on a module tag — it is already deferred. `async` still applies and overrides that: the module evaluates as soon as its whole graph has been fetched, with no ordering guarantee, and unlike classic scripts `async` also works on an **inline** `<script type="module">`. Two other loading differences matter: module fetches always use CORS mode, so a cross-origin module needs `Access-Control-Allow-Origin`, and the legacy `nomodule` attribute on a classic tag lets you ship a fallback that module-aware browsers skip.
code
html · 10 lines<head>
<!-- deferred by default; defer would be a no-op here -->
<script type="module" src="app.js"></script>
<!-- async is honoured, and works on an inline module too -->
<script type="module" async>
import { ping } from './telemetry.js';
ping();
</script>
</head>go deeper
Know that a module script does not block the parser and runs after the document is parsed, so it can safely live in the head and still see the DOM.
State the three attribute rules precisely: deferred by default, defer ignored, async honoured including on inline module tags. Mention the CORS-mode fetch as the practical difference from a classic tag.
Be ready to debug a real failure — a cross-origin module rejected for a missing header, or a deep import chain adding sequential round trips before anything evaluates — and say what you would change.
Own the delivery decision: whether to ship native modules directly, how deep an import graph you will tolerate on the critical path, and when the legacy nomodule fallback can finally be deleted from templates.
## The default flips The single most important fact is that `type="module"` changes the default from blocking to deferred: ```html <script src="app.js"></script> <!-- blocks the parser --> <script type="module" src="app.js"></script> <!-- deferred, never blocks --> ``` A module script is fetched in parallel with parsing and evaluated after the document has been parsed, before `DOMContentLoaded`. So a module in `<head>` is already doing the right thing, and its DOM queries will find the elements they are looking for. This holds for **inline** module scripts too. `<script type="module">init()</script>` in `<head>` does not run immediately the way an inline classic script does — it waits for parsing to finish. That asymmetry surprises people who have internalised "inline means synchronous". ## What the graph adds A module is not one file. Before evaluation the browser must fetch the entire dependency graph reachable from the entry file, then evaluate it bottom-up. Everything a module imports is therefore on the critical path for that module's execution: a chain of four imports is four sequential round trips unless the server pushes or the URLs are otherwise warmed. If two tags reference the same module URL, the browser fetches and evaluates it once and shares the result. ## defer on a module script `defer` has **no effect** on a module script. It is already deferred, and the attribute is simply ignored. Seeing `<script type="module" defer src="...">` in a codebase is harmless noise, not a second layer of postponement. ## async on a module script `async` does apply, and it means the same thing it means for a classic script: evaluate at the first opportunity after the fetch completes — here, after the whole graph is available — with no ordering guarantee relative to other scripts, and possibly before or after `DOMContentLoaded`. The one place it differs from classic behaviour is inline scripts: `async` is ignored on an inline classic script but honoured on an inline module script. ```html <script type="module" async> import { ping } from './telemetry.js'; ping(); </script> ``` That runs as soon as `telemetry.js` has been fetched, whether or not parsing has finished. ## The CORS difference Module scripts are always fetched in CORS mode. A classic `<script src="https://cdn.example.com/lib.js">` works with no special headers (which is why cross-origin classic script errors are opaque and reported as "Script error"). The same URL loaded as a module fails unless the server sends `Access-Control-Allow-Origin`. This is a real, frequent deployment bug when a team switches a CDN-hosted file from a classic tag to a module tag, and it is worth naming in an interview because it shows you have actually shipped modules. Relatedly, module fetches use the credentials rules of CORS, and the `crossorigin` attribute controls whether credentials are sent, exactly as it does for other CORS-mode subresources. ## nomodule and the legacy fallback `nomodule` is an attribute you put on a **classic** script. A browser that understands module scripts ignores any script carrying it; a browser that does not understand modules also does not understand `nomodule`, so it runs the script normally. That produces the module/nomodule pattern: ```html <script type="module" src="modern.js"></script> <script nomodule src="legacy.js" defer></script> ``` Modern browsers run `modern.js` only; a browser with no module support runs `legacy.js` only. This pattern mattered around 2017–2019 for shipping untranspiled code to browsers that could handle it. Every browser still receiving security updates supports modules, so today it is mostly a historical technique — worth recognising in old markup and knowing why it works, not worth adding to a new project. ## Putting it together If someone asks "what is the difference between defer and type=module", the honest answer is: for a single external file, the parser behaviour is nearly identical, because modules are deferred. The differences are the module *system* (a dependency graph fetched before evaluation, one evaluation per URL), the CORS-mode fetch, and the fact that `async` reaches inline scripts. Those are the details that separate a memorised answer from a used-it answer.
- Why does an inline module script behave differently from an inline classic script?An inline classic script runs immediately and ignores `async` and `defer`. An inline module script is deferred like any module, so it waits for parsing to finish, and it does honour `async`. Both differences follow from module scripts having their own loading mode rather than the legacy synchronous one.
- A CDN-hosted file worked as a classic script but fails after switching the tag to type="module". What is the likely cause?Module scripts are fetched in CORS mode, so the cross-origin response must carry `Access-Control-Allow-Origin`. A classic script tag has no such requirement. The fix is a CORS header on the CDN response, plus the `crossorigin` attribute if credentials are involved.
- What happens if two module tags on the page point at the same URL?The browser fetches and evaluates that module once and reuses it for both. Module URLs are keyed, so repeating a tag does not repeat the side effects — unlike two classic tags with the same `src`, which execute the file twice.
saying these in an interview costs you the question
- Thinks type=module blocks the parser like a classic script
- Adds defer to a module tag expecting extra delay
- Says async has no meaning on module scripts
- Assumes cross-origin modules load without CORS headers
- Believes nomodule goes on the module tag