HTML Guides for src
Learn how to identify and fix common HTML validation errors flagged by the W3C Validator — so your pages are standards-compliant and render correctly across every browser. Also check our Accessibility Guides.
The async attribute tells the browser to download and execute a script without blocking HTML parsing. For external scripts (those with a src attribute), this means the browser can continue parsing the page while fetching the file, then execute the script as soon as it's available. For inline module scripts (type="module"), async changes how the module's dependency graph is handled — the module and its imports execute as soon as they're all ready, rather than waiting for HTML parsing to complete.
For a classic inline script (no src, no type="module"), there is nothing to download asynchronously. The browser encounters the code directly in the HTML and executes it immediately. Applying async in this context is meaningless and contradicts the HTML specification, which is why the W3C validator flags it as an error.
Beyond standards compliance, using async incorrectly can signal a misunderstanding of script loading behavior, which may lead to bugs. For example, a developer might mistakenly believe that async on an inline script will defer its execution, when in reality it has no effect and the script still runs synchronously during parsing.
How to Fix
You have several options depending on your intent:
- If the script should be external, move the code to a separate file and reference it with the
srcattribute alongsideasync. - If the script should be an inline module, add
type="module"to the<script>tag. Note that module scripts are deferred by default, andasyncmakes them execute as soon as their dependencies are resolved rather than waiting for parsing to finish. - If the script is a plain inline script, simply remove the
asyncattribute — it has no practical effect anyway.
Examples
❌ Invalid: async on a classic inline script
<scriptasync>
console.log("Hello, world!");
</script>
This triggers the validator error because there is no src attribute and the type is not "module".
✅ Fixed: Remove async from the inline script
<script>
console.log("Hello, world!");
</script>
✅ Fixed: Use async with an external script
<scriptasyncsrc="app.js"></script>
The async attribute is valid here because the browser needs to fetch app.js from the server, and async controls when that downloaded script executes relative to parsing.
✅ Fixed: Use async with an inline module
<scriptasynctype="module">
import{greet}from"./utils.js";
greet();
</script>
This is valid because module scripts have a dependency resolution phase that can happen asynchronously. The async attribute tells the browser to execute the module as soon as all its imports are resolved, without waiting for the document to finish parsing.
❌ Invalid: async with type="text/javascript" (not a module)
<scriptasynctype="text/javascript">
console.log("This is still invalid.");
</script>
Even though type is specified, only type="module" satisfies the requirement. The value "text/javascript" is the default classic script type and does not make async valid on an inline script.
The charset attribute on the <script> element tells the browser what character encoding to use when interpreting the referenced external script file. When a script is written directly inside the HTML document (an inline script), the script's character encoding is inherently the same as the document's encoding — there is no separate file to decode. Because of this, the HTML specification requires that charset only appear on <script> elements that also have a src attribute pointing to an external file.
Including charset without src violates the HTML specification and signals a misunderstanding of how character encoding works for inline scripts. Validators flag this because browsers ignore the charset attribute on inline scripts, which means it has no effect and could mislead developers into thinking they've set the encoding when they haven't.
It's also worth noting that the charset attribute on <script> is deprecated in the HTML living standard, even for external scripts. The modern best practice is to serve external script files with the correct Content-Type HTTP header (e.g., Content-Type: application/javascript; charset=utf-8) or to simply ensure all your files use UTF-8 encoding, which is the default. If you're working with an older codebase that still uses charset, consider removing it entirely and relying on UTF-8 throughout.
Examples
Incorrect: charset on an inline script
This triggers the validation error because charset is specified without a corresponding src attribute.
<scriptcharset="utf-8">
console.log("Hello, world!");
</script>
Correct: Remove charset from inline scripts
Since inline scripts use the document's encoding, simply remove the charset attribute.
<script>
console.log("Hello, world!");
</script>
Correct: Use charset with an external script (deprecated but valid)
If you need to specify the encoding of an external script, both charset and src must be present. Note that this usage, while valid, is deprecated.
<scriptsrc="app.js"charset="utf-8"></script>
Recommended: External script without charset
The preferred modern approach is to omit charset entirely and ensure the server delivers the file with the correct encoding header, or simply use UTF-8 for everything.
<scriptsrc="app.js"></script>
The defer and async boolean attributes control how and when an external script is fetched and executed relative to HTML parsing. These attributes exist specifically to optimize the loading of external resources. An inline <script> block (one without a src attribute) doesn't need to be "downloaded" — its content is already embedded in the HTML document. Because of this, the defer attribute has no meaningful effect on inline scripts, and the HTML specification explicitly forbids this combination.
According to the WHATWG HTML living standard, the defer attribute "must not be specified if the src attribute is not present." Browsers will simply ignore the defer attribute on inline scripts, which means the script will execute synchronously as if defer were never added. This can mislead developers into thinking their inline script execution is being deferred when it isn't, potentially causing subtle timing bugs that are difficult to diagnose.
The same rule applies to the async attribute — it also requires the presence of a src attribute to be valid.
How to fix it
You have two options depending on your situation:
- If the script should be deferred, move the inline code into an external
.jsfile and reference it with thesrcattribute alongsidedefer. - If the script must remain inline, remove the
deferattribute entirely. If you need deferred execution for inline code, consider placing the<script>element at the end of the<body>, or useDOMContentLoadedto wait for the document to finish parsing.
Examples
❌ Invalid: defer on an inline script
<scriptdefer>
console.log("hello");
</script>
This triggers the validation error because defer is present without a corresponding src attribute.
✅ Fix option 1: Add a src attribute
Move the JavaScript into an external file (e.g., app.js) and reference it:
<scriptdefersrc="app.js"></script>
The browser will download app.js in parallel with HTML parsing and execute it only after the document is fully parsed.
✅ Fix option 2: Remove defer from the inline script
If the script must stay inline, simply remove the defer attribute:
<script>
console.log("hello");
</script>
✅ Fix option 3: Use DOMContentLoaded for deferred inline execution
If you need your inline script to wait until the DOM is ready, wrap the code in a DOMContentLoaded event listener:
<script>
document.addEventListener("DOMContentLoaded",function(){
console.log("DOM is fully parsed");
});
</script>
This achieves a similar effect to defer but is valid for inline scripts.
❌ Invalid: async on an inline script
The same rule applies to async:
<scriptasync>
document.title="Updated";
</script>
✅ Fixed
<scriptasyncsrc="update-title.js"></script>
Or simply remove async if the script is inline:
<script>
document.title="Updated";
</script>
Validate at scale.
Ship accessible websites, faster.
Automated HTML & accessibility validation for large sites. Check thousands of pages against WCAG guidelines and W3C standards in minutes, not days.
Pro Trial
Full Pro access. Cancel anytime.
Start Pro Trial →Join teams across 40+ countries