HTML Guides for script
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 HTML living standard defines that scripts with type="module" are always fetched in parallel and evaluated after the document has been parsed, which is the same behavior that the defer attribute provides for classic scripts. Because this deferred execution is an inherent characteristic of module scripts, the spec explicitly forbids combining the two. Including both doesn't change how the browser handles the script, but it signals a misunderstanding of how modules work and produces invalid HTML.
This validation error commonly arises when developers migrate classic scripts to ES modules. A classic script like <script defer src="app.js"></script> relies on the defer attribute to avoid blocking the parser. When converting to a module by adding type="module", it's natural to leave defer in place — but it's no longer needed or allowed.
It's worth noting that the async attribute is valid on module scripts and does change their behavior. While defer is redundant because modules are already deferred, async overrides that default and causes the module to execute as soon as it and its dependencies have finished loading, rather than waiting for HTML parsing to complete.
How to Fix
Remove the defer attribute from any <script> element that has type="module". No other changes are needed — the loading and execution behavior will remain identical.
If you intentionally want the script to run as soon as possible (before parsing completes), use async instead of defer. But if you want the standard deferred behavior, simply omit both attributes and let the module default take effect.
Examples
❌ Incorrect: defer combined with type="module"
<script type="module" defer src="app.js"></script>
The defer attribute is redundant here and causes a validation error.
✅ Correct: module script without defer
<script type="module" src="app.js"></script>
Module scripts are deferred automatically, so this behaves exactly the same as the incorrect example above but is valid HTML.
✅ Correct: using async with a module (when needed)
<script type="module" async src="analytics.js"></script>
Unlike defer, the async attribute is permitted on module scripts. It causes the module to execute as soon as it's ready, without waiting for HTML parsing to finish.
✅ Correct: classic script with defer
<script defer src="app.js"></script>
For classic (non-module) scripts, the defer attribute is valid and necessary if you want deferred execution.
The <script> element serves two distinct purposes in HTML: loading executable scripts and embedding non-executable data blocks. When the src attribute is present, the element is always being used to load an external script, so the type attribute must reflect a valid script type. Setting type to something like "text/html", "text/plain", or an invented value like "wrong" tells the browser this is not JavaScript, which means the external file referenced by src will be fetched but never executed — almost certainly not what the author intended.
The HTML specification restricts the allowed type values for <script src="..."> to three categories:
- An empty string (
type=""): Treated the same as the default, which is JavaScript. - A JavaScript MIME type: This includes
text/javascript,application/javascript, and other legacy JavaScript MIME types. Sincetext/javascriptis the default, specifying it is redundant. module: Indicates the script should be treated as a JavaScript module, enablingimport/exportsyntax and deferred execution by default.
Any value outside these categories — such as text/html, application/json, or a made-up string — is invalid when src is present.
Why this matters
Broken functionality: A non-JavaScript type on a <script> with src prevents the browser from executing the loaded file. The script is effectively dead code that still costs a network request.
Standards compliance: The HTML living standard explicitly forbids this combination. Validators flag it because it almost always indicates a mistake — either the wrong type was applied, or the src attribute was added by accident.
Maintainability: Future developers reading the code may be confused about whether the script is supposed to execute or serve as an inert data block. Keeping markup valid makes intent clear.
How to fix it
- Remove the
typeattribute entirely. This is the best approach for classic JavaScript. The default behavior istext/javascript, so notypeis needed. - Use
type="module"if the script uses ES module syntax (import/export). - If you intended a data block (e.g., embedding JSON or a template), remove the
srcattribute and place the content inline inside the<script>element instead. Data blocks with non-JavaScript types cannot usesrc.
Examples
Invalid: non-JavaScript types with src
These all trigger the validation error because the type value is not a JavaScript MIME type, an empty string, or "module":
<script type="text/html" src="template.html"></script>
<script type="application/json" src="data.json"></script>
<script type="text/plain" src="app.js"></script>
<script type="wrong" src="app.js"></script>
Valid: omitting the type attribute
The simplest and recommended fix for classic scripts — just drop type:
<script src="app.js"></script>
Valid: using a JavaScript MIME type
This is valid but redundant, since text/javascript is already the default. The validator may suggest omitting it:
<script type="text/javascript" src="app.js"></script>
Valid: using type="module"
Use this when the external script uses ES module syntax:
<script type="module" src="app.js"></script>
Valid: using an empty type attribute
An empty string is treated as the default. It's valid but unnecessary, and the validator may suggest removing it:
<script type="" src="app.js"></script>
Valid: data blocks without src
If you need a non-JavaScript type for an inline data block, remove the src attribute and place the content directly inside the element:
<script type="application/json" id="config">
{
"apiUrl": "https://example.com/api",
"debug": false
}
</script>
The crossorigin attribute is invalid on a <script> element whose type marks it as a data block, because browsers never fetch a data block over the network.
When the type attribute is anything other than a JavaScript MIME type, module, importmap, or speculationrules, the browser treats the element as a data block: inline content that other scripts on the page can read, but that never runs. The most common example is application/ld+json structured data. A data block can't have a src attribute either, so the browser makes no request for it.
crossorigin sets the CORS mode the browser uses when it downloads an external script, and for classic scripts it also decides whether error details from another origin reach window.onerror. With no download and no execution, the attribute has nothing to apply to.
This usually happens when a build step or plugin adds crossorigin to every <script> tag on the page, including the data blocks.
Invalid example
<script type="application/ld+json" crossorigin="anonymous">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Example"
}
</script>
Valid example
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Example"
}
</script>
<script src="https://cdn.example.com/app.js" crossorigin="anonymous"></script>
Remove crossorigin from the data block and keep it on the scripts the browser actually downloads. If the element was meant to run, fix the type instead: use module, a JavaScript MIME type such as text/javascript, or leave type out.
The defer attribute is invalid on a <script> element whose type marks it as a data block, because browsers never execute data blocks.
When the type attribute is anything other than a JavaScript MIME type, module, importmap, or speculationrules, the browser treats the element as a data block: inline content that other scripts on the page can read, but that is never fetched or run. The most common example is application/ld+json structured data. Since nothing executes, there is nothing to defer, and the attribute is invalid.
This usually happens when a template or a performance plugin adds defer to every <script> tag on the page, including the data blocks.
Examples
Invalid: defer on a data block
<script type="application/ld+json" defer>
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Example"
}
</script>
Valid: data block without defer
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Example"
}
</script>
Remove the defer attribute. If the script was actually meant to run, fix the type value instead: use a JavaScript MIME type, module, or omit the attribute entirely.
The fetchpriority attribute is invalid on a <script> element whose type marks it as a data block, because the browser never downloads a data block and has no request to prioritize.
When the type attribute is anything other than a JavaScript MIME type, module, importmap, or speculationrules, the browser treats the element as a data block: inline content that other scripts on the page can read, but that never runs. JSON-LD structured data in type="application/ld+json" is the most common example.
fetchpriority takes high, low or auto and tells the browser how urgently to download an external script compared with other resources on the page. A data block can't have a src and the browser never requests it, so the hint has nothing to apply to.
The markup is often valid when the server sends it. Some script optimizers rewrite the type so the browser skips the script on load, then run it later with their own loader. Cloudflare Rocket Loader, for example, turns type="module" into a prefixed value such as type="a1b2c3d4-module". The validator checks the rewritten markup, finds an unknown type, and reports the fetchpriority and src attributes as well as the type value itself.
Invalid example
<script type="a1b2c3d4-module" src="/js/gallery.js" fetchpriority="low"></script>
Valid example
<script data-cfasync="false" type="module" src="/js/gallery.js" fetchpriority="low"></script>
data-cfasync="false" tells Rocket Loader to leave the script alone, so the browser receives the original type="module". Cloudflare requires it before the src attribute. Turning Rocket Loader off for the site also works.
If the element really is a data block, such as JSON-LD, remove fetchpriority from it. If it was meant to run and the type holds a typo, fix the type instead: use module, a JavaScript MIME type such as text/javascript, or leave type out.
The src attribute is invalid on a <script> element whose type marks it as a data block, because the data lives in the element itself and there is nothing to fetch from elsewhere.
When the type attribute is anything other than a JavaScript MIME type, module, importmap, or speculationrules, the browser treats the element as a data block: inline content that other scripts on the page can read, but that is never downloaded or run. The most common example is application/ld+json structured data. The whole point of a data block is that it carries its content between the opening and closing tags, so pointing src at an external file makes no sense and is not allowed.
This usually happens when the type is set for a data block but the markup was copied from a real external script that still carries its src.
Examples
Invalid: src on a data block
<script type="application/ld+json" src="https://example.com/data.json"></script>
Valid: data block with inline content
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Example"
}
</script>
Remove the src attribute and place the data inline. If the file really is a script to load and run, fix the type value instead: use a JavaScript MIME type, module, or omit the attribute entirely.
The async attribute is invalid on a <script> element whose type marks it as a data block, because browsers never fetch or execute data blocks.
When the type attribute is anything other than a JavaScript MIME type, module, importmap, or speculationrules, the browser treats the element as a data block: inline content that other scripts on the page can read, but that is never downloaded or run. The most common example is application/ld+json structured data. async controls when a script downloads and executes, so it has no meaning on an element that does neither.
This usually happens when a template or a performance plugin adds async to every <script> tag on the page, including the data blocks.
Examples
Invalid: async on a data block
<script type="application/ld+json" async>
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Example"
}
</script>
Valid: data block without async
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Example"
}
</script>
Remove the async attribute. If the script was actually meant to run, fix the type value instead: use a JavaScript MIME type, module, or omit the attribute entirely.
The defer attribute has no effect on <script> elements that have type="module" because module scripts are already deferred by default.
When a browser encounters <script type="module">, it automatically fetches the script in parallel with HTML parsing and executes it after the document has been parsed. This is the same behavior that defer provides for classic scripts. Adding defer to a module script is redundant, and the W3C validator flags it as invalid markup.
Classic scripts (<script src="...">) block HTML parsing by default. The defer attribute changes that behavior so the script loads in parallel and runs after parsing completes. Module scripts adopted this deferred behavior as their default, so the attribute is unnecessary.
The async attribute, on the other hand, does have an effect on module scripts. It causes the module to execute as soon as it finishes loading, rather than waiting for parsing to complete.
Examples
Invalid: defer on a module script
<script type="module" src="app.js" defer></script>
Valid: module script without defer
<script type="module" src="app.js"></script>
Remove the defer attribute. The script will behave exactly the same way, since module scripts are deferred by default.
The async attribute is invalid on an inline classic <script>, because there is no external file for the browser to fetch in the background.
async tells the browser to download a script while it keeps parsing the page, and to run it as soon as it arrives. The code of an inline script is already in the page, so the browser runs it right away and async changes nothing. A classic script is any <script> whose type is missing, empty or a JavaScript MIME type such as text/javascript.
Inline module scripts are the exception. On <script type="module">, async makes the module run as soon as it and its imports are ready, instead of waiting until the document has been parsed, so the validator accepts it there.
Templates and performance plugins that add async to every <script> cause most of these. Remove the attribute from the inline script, or move the code to an external file if it should load without blocking the parser.
Invalid example
<script async>
console.log("Hello");
</script>
Valid example
<script>
console.log("Hello");
</script>
To load the code without blocking the parser, put it in an external file and keep async on that <script>:
<script src="app.js" async></script>
The defer attribute is invalid on an inline <script>, because it only affects scripts that are loaded from an external file through the src attribute.
defer tells the browser to download an external script in parallel with parsing and run it after the document has finished parsing. An inline script has no external file to download, so there is nothing to defer and the browser runs it as soon as it is parsed. Because the attribute has no defined effect here, the validator flags it.
This usually happens when a template or a performance plugin adds defer to every <script> on the page, including the inline ones. async gets a similar error on inline scripts, except on inline module scripts (type="module"), where it is allowed.
Invalid example
<script defer>
console.log("Hello");
</script>
Valid example
Remove the defer attribute from the inline script:
<script>
console.log("Hello");
</script>
If you need the code to run after the document is parsed, move it into an external file and defer that instead:
<script src="app.js" defer></script>
Understanding the Issue
MIME types follow a specific format: a type and a subtype separated by a forward slash, such as text/javascript or application/json. When the W3C validator encounters type="rocketlazyloadscript" on a <script> element, it flags it because this value has no slash and no subtype — it doesn't conform to the MIME type syntax defined in the HTML specification.
The value rocketlazyloadscript is intentionally set by the WP Rocket WordPress caching and performance plugin. WP Rocket changes the type attribute of <script> elements from text/javascript (or removes the default) and replaces it with this custom value. This prevents the browser from executing the script immediately on page load. WP Rocket's JavaScript then swaps the type back to a valid value when the script should actually be loaded, achieving a lazy-loading effect that can improve page performance.
Why This Is Flagged
The HTML specification states that if the type attribute is present on a <script> element, its value must be one of the following:
- A valid JavaScript MIME type (e.g.,
text/javascript) — indicating the script should be executed. - The string
module— indicating the script is a JavaScript module. - Any other valid MIME type that is not a JavaScript MIME type — indicating a data block that the browser should not execute.
Since rocketlazyloadscript is not a valid MIME type at all (it lacks the type/subtype structure), it fails validation. While browsers will simply ignore scripts with unrecognized type values (which is exactly what WP Rocket relies on), the markup itself is technically non-conforming.
How to Fix It
Because this value is generated by a plugin rather than authored manually, your options are:
Configure WP Rocket to exclude specific scripts — In WP Rocket's settings under "File Optimization," you can exclude individual scripts from the "Delay JavaScript execution" feature. This prevents WP Rocket from modifying their
typeattribute.Disable delayed JavaScript execution entirely — If W3C compliance is critical, you can turn off the "Delay JavaScript execution" option in WP Rocket. This eliminates the validation errors but sacrifices the performance benefit.
Accept the validation errors — WP Rocket acknowledges these errors in their documentation and considers them an expected side effect of their optimization technique. Since browsers handle the non-standard
typegracefully (by not executing the script), there is no functional or accessibility issue. The errors are purely a standards compliance concern.
Examples
Invalid: Non-standard type value (generated by WP Rocket)
<script type="rocketlazyloadscript" src="/js/analytics.js"></script>
The validator reports: Bad value "rocketlazyloadscript" for attribute "type" on element "script": Subtype missing.
Valid: Standard type attribute
<script type="text/javascript" src="/js/analytics.js"></script>
Valid: Omitting the type attribute entirely
Since text/javascript is the default for <script> elements in HTML5, the type attribute can be omitted entirely:
<script src="/js/analytics.js"></script>
Valid: Using a module script
<script type="module" src="/js/app.js"></script>
Valid: Using a data block with a proper MIME type
If you need a non-executed data block, use a valid MIME type that isn't a JavaScript type:
<script type="application/json" id="config">
{"lazy": true, "threshold": 200}
</script>
The defer attribute on the <script> element is a boolean attribute and should not be assigned a value like "true".
In HTML, boolean attributes don't work like regular attributes. A boolean attribute is considered "true" simply by being present on the element, and "false" by being omitted entirely. Setting defer="true" is invalid because the only allowed values for a boolean attribute are either the empty string ("") or the attribute's own name (e.g., defer="defer").
The defer attribute tells the browser to download the script in parallel with HTML parsing and execute it only after the document has been fully parsed. It only works on scripts with a src attribute — it has no effect on inline scripts.
Bad Example
<script src="app.js" defer="true"></script>
Good Example
<script src="app.js" defer></script>
This rule applies to all boolean attributes in HTML, such as disabled, checked, readonly, async, hidden, and others. Just include the attribute name by itself — no value needed.
The HTML specification defines boolean attributes as attributes whose presence indicates a true state and whose absence indicates false. According to the WHATWG HTML standard, a boolean attribute may only have three valid representations:
- The attribute name alone (e.g.,
async) - The attribute with an empty string value (e.g.,
async="") - The attribute with a value matching its own name, case-insensitively (e.g.,
async="async")
Any other value — such as async="true", async="1", async="yes", or async="false" — is invalid HTML and will trigger this validation error. This is a common misunderstanding because developers often assume boolean attributes work like boolean values in programming languages, where you'd assign true or false.
Why this matters
While most browsers are lenient and will treat any value of async as the true state (since the attribute is present regardless of its value), using invalid values creates several problems:
- Standards compliance: Invalid HTML may cause issues with strict parsers, validators, or tools that process your markup.
- Misleading intent: Writing
async="false"does not disable async behavior — the attribute is still present, so the browser treats it as enabled. This can lead to confusing bugs where a script behaves asynchronously even though the developer intended otherwise. - Maintainability: Other developers reading the code may misinterpret
async="false"as actually disabling async loading.
To disable async behavior, you must remove the attribute entirely rather than setting it to "false".
How async works
For classic scripts with a src attribute, the async attribute causes the script to be fetched in parallel with HTML parsing and executed as soon as it's available, without waiting for the document to finish parsing.
For module scripts (type="module"), the async attribute causes the module and all its dependencies to be fetched in parallel and executed as soon as they are ready, rather than waiting until the document has been parsed (which is the default deferred behavior for modules).
Examples
❌ Invalid: arbitrary values on async
<!-- Bad: "true" is not a valid boolean attribute value -->
<script async="true" src="app.js"></script>
<!-- Bad: "1" is not a valid boolean attribute value -->
<script async="1" src="analytics.js"></script>
<!-- Bad: "yes" is not a valid boolean attribute value -->
<script async="yes" src="tracker.js"></script>
<!-- Bad and misleading: this does NOT disable async -->
<script async="false" src="app.js"></script>
✅ Valid: correct boolean attribute usage
<!-- Preferred: attribute name alone -->
<script async src="app.js"></script>
<!-- Also valid: empty string value -->
<script async="" src="app.js"></script>
<!-- Also valid: value matching attribute name -->
<script async="async" src="app.js"></script>
<!-- Correct way to disable async: remove the attribute -->
<script src="app.js"></script>
✅ Valid: async with module scripts
<script async type="module" src="app.mjs"></script>
<script async type="module">
import { init } from './utils.mjs';
init();
</script>
This same rule applies to all boolean attributes in HTML, including defer, disabled, checked, required, hidden, and others. When in doubt, use the attribute name on its own with no value — it's the cleanest and most widely recognized form.
The integrity attribute enables Subresource Integrity (SRI), a security feature that lets browsers verify that a fetched resource (such as a JavaScript file from a CDN) has not been tampered with. The attribute value follows the format [algorithm]-[base64-encoded hash], where the algorithm is typically sha256, sha384, or sha512, and the hash is a base64 representation of the file's cryptographic digest.
Base64 encoding works by converting binary data into a string of ASCII characters drawn from a 64-character alphabet. A key property of valid base64 is that the encoded output must always have a length that is a multiple of 4. When the raw binary data doesn't divide evenly, the output is padded with one or two = characters to reach the correct length. If the base64 string in your integrity attribute has an incorrect length — for instance, it was truncated, manually edited, or copied incompletely — the validator will flag it as invalid.
This matters for several reasons:
- Security: If the
integrityvalue is malformed, the browser cannot verify the resource. Depending on the browser, it may block the script entirely, breaking your site's functionality. - Standards compliance: The HTML specification requires the hash portion to be a valid base64 string. An invalid value is a conformance error.
- Reliability: A malformed hash will never match any file, so the SRI check will always fail, effectively making the script unusable.
Common causes of this error include:
- Copying the hash value incompletely (missing trailing
=padding or other characters). - Manually modifying the hash string.
- Using a tool that produced an incorrectly encoded output.
- Mixing up base64 and base64url encodings (base64url uses
-and_instead of+and/, and often omits padding).
To fix the issue, regenerate the correct SRI hash for the exact file being referenced. You can do this with the command line:
openssl dgst -sha384 -binary script.js | openssl base64 -A
Or using shasum and base64:
shasum -b -a 384 script.js | awk '{print $1}' | xxd -r -p | base64
Online tools like the SRI Hash Generator can also produce the correct value. After generating the hash, prepend the algorithm prefix (e.g., sha384-) and verify that the base64 portion has a length divisible by 4.
Examples
Incorrect: Malformed base64 value
The hash below is not a valid base64 string — its length is not a multiple of 4, and it contains the character !, which is not in the base64 alphabet.
<script
src="https://cdn.example.com/library.js"
integrity="sha384-BadBase64Value!"
crossorigin="anonymous"></script>
Incorrect: Truncated hash missing padding
This hash has been accidentally truncated, losing the trailing = padding characters that make it a valid base64 string.
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9Gh8S7f1bE0q/PuF3LtHac+obYTK2B69B1a8t"
crossorigin="anonymous"></script>
Correct: Properly formatted SRI hash
The base64 hash is the correct length (a multiple of 4) and uses only valid base64 characters. The trailing T completes the final 4-character group without needing padding in this case, but other hashes may end with = or ==.
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9Gh8S7f1bE0q/PuF3LtHac+obYTK2B69B1a8tT"
crossorigin="anonymous"></script>
Correct: Multiple hashes for fallback
You can provide multiple hash values separated by spaces. Each one must be independently valid base64 with the correct length.
<script
src="https://cdn.example.com/library.js"
integrity="sha256-BpfGp+xz0kN9mMKoECQbGRab7fMi/dsRZ7Sy7LVd3CE= sha384-oqVuAfXRKap7fdgcCY5uykM6+R9Gh8S7f1bE0q/PuF3LtHac+obYTK2B69B1a8tT"
crossorigin="anonymous"></script>
Always ensure the hash is generated from the exact file the src attribute points to. If the CDN serves a different version or a minified variant, the hash will not match, and the browser will block the resource regardless of whether the base64 encoding is valid.
The src attribute on a <script> element points to a URL whose path contains a character that is not allowed there unless it is percent-encoded.
A URL path may only use a restricted set of characters. Symbols such as ^, spaces, square brackets, and backticks have to be written in their percent-encoded form, so a caret becomes %5E. The validator names the exact character it rejected, so look at the path segment it points to and either encode that character or change the URL so it no longer appears.
A common source of the ^ character is a version range copied from a package manager. A specifier like @^2 belongs in a package.json file, not in a CDN URL. When you load a script straight from a CDN, pin an explicit version instead.
Invalid example
<script src="https://cdn.example.com/pkg@^2/dist/app.js"></script>
Valid example
<script src="https://cdn.example.com/pkg@2.1.0/dist/app.js"></script>
URLs follow strict syntax rules defined by RFC 3986, which does not permit literal space characters anywhere in the URI — including path segments, query strings, and fragment identifiers. When the W3C HTML Validator encounters a space in the src attribute of a <script> element, it flags it as an illegal character because the attribute value is not a valid URL.
While most modern browsers will silently fix this by encoding the space before making the request, relying on this behavior is problematic for several reasons:
- Standards compliance: The HTML specification requires that the
srcattribute contain a valid URL. A URL with a literal space is technically malformed. - Cross-browser reliability: Not all user agents, proxies, or CDNs handle malformed URLs the same way. What works in one browser may fail in another context.
- Interoperability: Other tools that consume your HTML — such as linters, crawlers, screen readers, and build pipelines — may not be as forgiving as browsers.
- Copy-paste and linking issues: Literal spaces in URLs cause problems when users copy links or when URLs appear in plain-text contexts like emails, where the space may break the URL in two.
How to fix it
You have three options, listed from most recommended to least:
- Rename the file or directory to eliminate spaces entirely (e.g., use hyphens or underscores). This is the cleanest solution.
- Percent-encode the space as
%20in thesrcattribute value. - Use a build tool or bundler that generates references with properly encoded or space-free paths automatically.
Avoid using + as a space replacement in path segments. The + character represents a space only in application/x-www-form-urlencoded query strings, not in URL path segments.
Examples
❌ Invalid: space in the path segment
<script src="https://example.com/media assets/app.js"></script>
The space between media and assets makes this an invalid URL.
✅ Fixed: percent-encode the space
<script src="https://example.com/media%20assets/app.js"></script>
Replacing the space with %20 produces a valid, standards-compliant URL.
✅ Better: rename to avoid spaces entirely
<script src="https://example.com/media-assets/app.js"></script>
Using a hyphen (or underscore) instead of a space is the preferred approach. It keeps URLs clean, readable, and free of encoding issues.
❌ Invalid: space in a local relative path
This issue isn't limited to absolute URLs. Relative paths trigger the same error:
<script src="js/my script.js"></script>
✅ Fixed: encode or rename the local file
<script src="js/my%20script.js"></script>
Or, better yet:
<script src="js/my-script.js"></script>
Multiple spaces and other special characters
If a URL contains multiple spaces or other special characters, each one must be individually encoded. For example, { becomes %7B and } becomes %7D. A quick reference for common characters:
| Character | Encoded form |
|---|---|
| Space | %20 |
[ | %5B |
] | %5D |
{ | %7B |
} | %7D |
<!-- Invalid -->
<script src="libs/my library [v2].js"></script>
<!-- Valid -->
<script src="libs/my%20library%20%5Bv2%5D.js"></script>
<!-- Best: rename the file -->
<script src="libs/my-library-v2.js"></script>
Note that this same rule applies to the src attribute on other elements like <img>, <iframe>, <audio>, and <video>, as well as the href attribute on <a> and <link>. Whenever you reference a URL in HTML, make sure it contains no literal spaces.
A { in the query string of a <script> element's src is not a valid URL character, and it almost always comes from a template placeholder that was never replaced.
Curly braces can appear in a URL only when they're percent-encoded as %7B and %7D. In a script URL they usually belong to a cache-busting parameter such as ?v=${build.version} or ?v={{version}}. The template engine or the build step should have swapped the placeholder for a version number or a timestamp, but the raw text reached the page instead. A misspelled variable name, or a template file the build step doesn't process, leaves it behind.
Browsers still load the script, sending the placeholder text as part of the query. The cache buster stops doing its job, though: the URL stays the same after every deploy, so browsers and CDNs can keep serving an old copy of the file.
Fix the template or the build configuration so the placeholder renders as a real value. If the braces are meant to be literal, percent-encode them.
Invalid example
<script src="/js/app.js?v=${build.version}"></script>
Valid example
<script src="/js/app.js?v=2.4.1"></script>
When the browser's HTML parser encounters a src attribute, it expects the value between the opening and closing quotes to be a valid URL. URLs have strict rules about which characters are permitted — a literal double quote (") is not one of them. If a " character appears inside the URL, the validator flags it as an illegal character in the query string (or other parts of the URL).
This error often doesn't stem from an intentionally embedded quote in the URL itself. Instead, it's usually a symptom of malformed HTML around the attribute. Common causes include:
- Stray quotes after the attribute value, such as writing
src="https://example.com/script.js"async""instead of properly separating attributes. - Accidentally doubled closing quotes, like
src="https://example.com/script.js"". - Boolean attributes given empty-string values incorrectly, such as
async""instead of justasync, which causes the parser to interpret the extra quotes as part of the precedingsrcvalue. - Copy-paste errors that introduce smart quotes (
"") or extra quotation marks into the URL.
This matters for several reasons. Malformed src attributes can cause the script to fail to load entirely, breaking functionality on your page. It also violates the HTML specification, which can lead to unpredictable behavior across different browsers as each parser tries to recover from the error differently.
To fix the issue, carefully inspect the <script> tag and ensure that:
- The
srcattribute value contains only a valid URL with no unescaped"characters. - The opening and closing quotes around the attribute value are properly balanced.
- Attributes are separated by whitespace — not jammed together.
- Boolean attributes like
asyncanddeferare written as bare keywords without values.
If you genuinely need a double quote character in a query string (which is rare), encode it as %22.
Examples
Incorrect — stray quotes between attributes
In this example, the async attribute is malformed as async"", which causes the parser to interpret the extra quotes as part of the src URL:
<script src="https://example.com/js/app.js?ver=3.1" async""></script>
Incorrect — doubled closing quote
Here, an extra " at the end of the src value creates an illegal character in the URL:
<script src="https://example.com/js/app.js?ver=3.1""></script>
Incorrect — smart quotes from copy-paste
Curly/smart quotes copied from a word processor are not valid HTML attribute delimiters and get treated as part of the URL:
<script src="https://example.com/js/app.js?ver=3.1"></script>
Correct — clean attributes with proper quoting
<script src="https://example.com/js/app.js?ver=3.1" async></script>
Correct — boolean attribute written as a bare keyword
Boolean attributes like async and defer don't need a value. Simply include the attribute name:
<script src="https://example.com/js/app.js?ver=3.1" defer></script>
While async="async" is technically valid per the spec, the cleanest and most common form is the bare attribute. Avoid empty-string patterns like async="" placed without proper spacing, as they can lead to the exact quoting errors described here.
Correct — full valid document
<!DOCTYPE html>
<html lang="en">
<head>
<title>Valid Script Example</title>
<script src="https://example.com/js/app.js?ver=3.1" async></script>
</head>
<body>
<p>Page content here.</p>
</body>
</html>
If you're generating <script> tags dynamically (through a CMS, template engine, or build tool), check the template source rather than just the rendered output. The stray quotes are often introduced by incorrect string concatenation or escaping logic in the server-side code.
The type attribute of a <script> element has extra text after its JavaScript MIME type, such as a ; charset=utf-8 parameter or a piece of another attribute.
When a <script> has a type, the HTML standard tells browsers to run it as JavaScript only if the value is exactly a JavaScript MIME type like text/javascript. Letter case and surrounding spaces don't matter, but parameters aren't allowed. Any other value turns the element into a data block, which the browser never runs. So a <script> with src often gets a second error too: data blocks can't have a src attribute.
There are two common causes. Older code sometimes copied ; charset=utf-8 from an HTTP Content-Type header, where parameters are allowed, into the type attribute, where they aren't. Or someone typed text/javascript" async="async into a CMS field or plugin setting that fills in type, hoping to add a second attribute. The template escaped the quotes as ", so async stayed inside the type value.
For JavaScript, remove type. The default is already text/javascript, and the validator reports an explicit one as unnecessary. Write other options like async or defer as separate attributes. If the script file needs a declared encoding, set it in the Content-Type header the server sends for that file.
Invalid example
<script src="/js/app.js" type="text/javascript; charset=utf-8"></script>
<script src="/js/stats.js" type="text/javascript" async="async"></script>
Valid example
<script src="/js/app.js"></script>
<script src="/js/stats.js" async></script>
A MIME type (also called a media type) is composed of two parts: a type and a subtype, separated by a forward slash (/) with no whitespace. For example, text/javascript has text as the type and javascript as the subtype. When you specify a value like text or javascript alone — without the slash and the other component — the validator reports this error because the subtype is missing.
This error commonly occurs when authors confuse the MIME type format with a simple label, writing something like type="text" or type="javascript" instead of the full type="text/javascript". It can also happen due to a typo, such as accidentally omitting the slash or the subtype portion.
Why this matters
Browsers rely on the type attribute to determine how to process the contents of a <script> element. An invalid MIME type can cause browsers to misinterpret or skip the script entirely. While modern browsers default to JavaScript when no type is specified, providing a malformed MIME type is not the same as omitting it — it may lead to unpredictable behavior across different browsers and versions. Keeping your markup valid also ensures better tooling support and forward compatibility.
How to fix it
You have two main options:
- Provide a complete, valid MIME type. For JavaScript, use
text/javascript. For JSON data blocks, useapplication/json. For importmaps, useimportmap. - Remove the
typeattribute entirely. Per the HTML specification, the default type for<script>istext/javascript, so omittingtypeis perfectly valid and is actually the recommended approach for standard JavaScript.
Examples
Incorrect: missing subtype
<!-- "text" alone is not a valid MIME type -->
<script type="text" src="app.js"></script>
<!-- "javascript" alone is not a valid MIME type -->
<script type="javascript" src="app.js"></script>
Correct: full MIME type specified
<script type="text/javascript" src="app.js"></script>
Correct: omitting the type attribute (recommended for JavaScript)
<script src="app.js"></script>
Since text/javascript is the default, omitting the attribute is the cleanest approach for standard JavaScript files.
Correct: using type for non-JavaScript purposes
The type attribute is still useful when embedding non-JavaScript content in a <script> element. In these cases, always use the full MIME type:
<script type="application/json" id="config">
{"apiUrl": "https://example.com/api"}
</script>
<script type="importmap">
{ "imports": { "lodash": "/libs/lodash.js" } }
</script>
Common valid MIME types for <script>
| MIME Type | Purpose |
|---|---|
text/javascript | Standard JavaScript (default) |
module | JavaScript module |
importmap | Import map |
application/json | Embedded JSON data |
application/ld+json | Linked Data / structured data |
Note that module and importmap are special values defined by the HTML specification and are not traditional MIME types, but they are valid values for the type attribute on <script> elements.
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
<script async>
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
<script async src="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
<script async type="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)
<script async type="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.
<script charset="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.
<script src="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.
<script src="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
<script defer>
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:
<script defer src="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:
<script async>
document.title = "Updated";
</script>
✅ Fixed
<script async src="update-title.js"></script>
Or simply remove async if the script is inline:
<script>
document.title = "Updated";
</script>
A <script> element sits between the closing </head> tag and the opening <body> tag, where the HTML parser doesn't expect any elements.
After </head>, the parser expects the <body> start tag, with nothing but whitespace or comments before it. When it finds a <script> there, browsers move the element back into the <head> and run it from there, so the page usually works. The markup is still invalid. An analytics or tag manager snippet pasted right after </head> is a common cause, and so is a theme or plugin that prints its scripts there.
The validator treats this error as fatal. Right after it, the report shows "Cannot recover after last error. Any further errors will be ignored." and nothing later in the page gets checked. Other HTML problems below this point stay hidden until you fix it.
To fix it, move the <script> element inside the <head>, before </head>. Browsers already run it from there, so the page behaves the same after the change. A script can also go anywhere inside the <body>, for example right before </body>.
Invalid example
<!DOCTYPE html>
<html lang="en">
<head>
<title>My page</title>
</head>
<script src="analytics.js"></script>
<body>
<p>Hello, world!</p>
</body>
</html>
Valid example
<!DOCTYPE html>
<html lang="en">
<head>
<title>My page</title>
<script src="analytics.js"></script>
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
In a valid HTML document, all content must reside within the <html> element, and specifically within either <head> or <body>. The HTML parser expects a well-defined structure: <!DOCTYPE html>, then <html>, containing <head> and <body>. When a <script> tag appears outside this hierarchy — for example, after the closing </html> tag — the validator reports it as a "stray start tag" because it has no valid parent in the document tree.
This is a common issue that arises in a few ways. Sometimes developers accidentally place a script after </body> or </html>, thinking it will still execute. Other times, template systems or CMS platforms inject scripts at the end of the output without ensuring they're inside <body>. While browsers are forgiving and will typically still execute the script, relying on this error-recovery behavior leads to non-standard markup and unpredictable DOM placement.
Why this matters
- Standards compliance: The HTML specification requires all elements to be properly nested within the document structure. A
<script>outside<html>violates this requirement. - Predictable DOM: When a browser encounters a stray
<script>, it must use error recovery to determine where to place it in the DOM. Different browsers may handle this differently, leading to inconsistencies. - Maintainability: Invalid markup can cause confusing debugging scenarios, especially when JavaScript relies on DOM structure or ordering.
How to fix it
Move the <script> element inside either <head> or <body>:
- Place it in
<head>if the script needs to load before the page content renders (configuration, analytics setup, etc.). Consider using thedeferorasyncattribute for external scripts to avoid blocking rendering. - Place it at the end of
<body>(just before</body>) if the script interacts with DOM elements, which is a common and recommended pattern.
Examples
Incorrect: script after closing </html> tag
This triggers the "Stray start tag script" error because the <script> is outside the document structure entirely.
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Page</title>
</head>
<body>
<p>Hello world</p>
</body>
</html>
<script>
console.log("This is stray!");
</script>
Incorrect: script between </head> and <body>
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Page</title>
</head>
<script>
console.log("Misplaced script");
</script>
<body>
<p>Hello world</p>
</body>
</html>
Correct: script in the <head>
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Page</title>
<script>
console.log("Hello from the head");
</script>
</head>
<body>
<p>Hello world</p>
</body>
</html>
Correct: script at the end of <body>
This is the most common and recommended placement for scripts that interact with the page.
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Page</title>
</head>
<body>
<p>Hello world</p>
<script>
console.log("Hello from the body");
</script>
</body>
</html>
Correct: external script with defer in <head>
Using defer lets you place the script in <head> while ensuring it executes after the DOM is fully parsed, giving you the best of both worlds.
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Page</title>
<script src="app.js" defer></script>
</head>
<body>
<p>Hello world</p>
</body>
</html>
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.
1K Weekly
1,000 monthly checks. Cancel anytime.
Get 1K Weekly →Join teams across 40+ countries