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. Since text/javascript is the default, specifying it is redundant.
- module: Indicates the script should be treated as a JavaScript module, enabling import/export syntax 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 type attribute entirely. This is the best approach for classic JavaScript. The default behavior is text/javascript, so no type is 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 src attribute and place the content inline inside the <script> element instead. Data blocks with non-JavaScript types cannot use src.
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 src attribute on a <script> element tells the browser where to fetch an external JavaScript file. According to the HTML specification, when the src attribute is present, its value must be a valid non-empty URL. An empty string does not qualify as a valid URL, so the validator flags it as an error.
This issue typically arises in a few common scenarios:
- Templating or CMS placeholders — A template engine or content management system outputs an empty src when no script URL is configured.
- Dynamic JavaScript — Client-side code is intended to set the src later, but the initial HTML ships with an empty value.
- Copy-paste mistakes — The attribute was added in anticipation of a script file that was never specified.
Beyond failing validation, an empty src causes real problems. Most browsers interpret an empty src as a relative URL that resolves to the current page’s URL. This means the browser will make an additional HTTP request to re-fetch the current HTML document and attempt to parse it as JavaScript, wasting bandwidth, slowing down page load, and potentially generating console errors. It can also cause unexpected side effects in server logs and analytics.
How to Fix It
Choose the approach that matches your intent:
- Provide a valid URL — If you need an external script, set src to the correct file path or full URL.
- Use an inline script — If your JavaScript is written directly in the HTML, remove the src attribute entirely. Note that when src is present, browsers ignore any content between the opening and closing <script> tags.
- Remove the element — If the script isn’t needed, remove the <script> element altogether.
Also keep in mind:
- Ensure the file path is correct relative to the HTML file’s location.
- If using an absolute URL, verify it is accessible and returns JavaScript content.
- If a script should only be loaded conditionally, handle the condition in your server-side or build logic rather than outputting an empty src.
Examples
❌ Invalid: Empty src attribute
<script src=""></script>
This triggers the validation error because the src value is an empty string.
❌ Invalid: Whitespace-only src attribute
<script src=" "></script>
A value containing only whitespace is also not a valid URL and will produce the same error.
✅ Fixed: Valid external script
<script src="js/app.js"></script>
The src attribute contains a valid relative URL pointing to an actual JavaScript file.
✅ Fixed: Valid external script with a full URL
<script src="https://example.com/js/library.min.js"></script>
✅ Fixed: Inline script without src
If you want to write JavaScript directly in your HTML, omit the src attribute:
<script>
console.log("This is an inline script.");
</script>
✅ Fixed: Conditionally omitting the element
If the script URL comes from a template variable that might be empty, handle it at the template level so the <script> element is only rendered when a URL is available. For example, in a templating language:
<!-- Pseudocode — only output the tag when the URL exists -->
<!-- {% if script_url %} -->
<script src="analytics.js"></script>
<!-- {% endif %} -->
This prevents the empty src from ever reaching the browser.
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 type attribute.
-
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 type gracefully (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 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 value assigned to the integrity attribute in your script tag is not a valid base64-encoded string, which is required for Subresource Integrity (SRI).
The integrity attribute is used to ensure that the resource fetched by the browser matches the expected cryptographic hash. This value needs to be a valid base64-encoded hash, commonly generated using SHA-256, SHA-384, or SHA-512. The value format should be: [algorithm]-[base64-encoded hash]. The base64 part must have a length that is a multiple of 4, padded with = if necessary.
Example of incorrect usage:
<script
src="https://example.com/script.js"
integrity="sha384-BadBase64Value!"
crossorigin="anonymous"></script>
How to fix:
Obtain the correct hash for the file and use a valid, properly padded base64 value. You can generate an SRI hash using tools such as SRI Hash Generator or the command line.
Correct usage example:
<script
src="https://example.com/script.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9Gh8S7f1bE0q/PuF3LtHac+obYTK2B69B1a8tT"
crossorigin="anonymous"></script>
Always ensure the hash matches the actual file to avoid browser blocking, and that the base64 string is correctly formatted.
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 src attribute 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 %20 in the src attribute 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.
There is an illegal double quote character (") at the end of the src attribute value in your <script> tag, which causes the attribute to be invalid.
Attribute values must not include unescaped or stray quote characters (" or ') inside them, as this breaks attribute parsing and results in invalid HTML. The src attribute for a <script> tag should contain a properly encoded URL without any stray quotes or illegal characters. In your case, a double quote has accidentally been included before the closing quote of the attribute.
Correct usage for a <script> tag with the async attribute is:
<script src="https://example.com/js/jquery-3.6.0.min.js?ver=6.8.2" async></script>
Incorrect example with the error (shows the issue):
<script src="https://example.com/js/jquery-3.6.0.min.js?ver=6.8.2" async""></script>
Make sure there are no stray characters in your attribute values and that boolean attributes like async do not have values—it should simply be present, as in async, not async"" or async="async".
If you need a full, minimal HTML document to validate, use:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Valid Script Tag Example</title>
</head>
<body>
<script src="https://example.com/js/jquery-3.6.0.min.js?ver=6.8.2" async></script>
</body>
</html>
Double-check your HTML source code to ensure there are no accidental typos or misplaced quote marks in your tag attributes.
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, use application/json. For importmaps, use importmap.
- Remove the type attribute entirely. Per the HTML specification, the default type for <script> is text/javascript, so omitting type is 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 src attribute alongside async.
- If the script should be an inline module, add type="module" to the <script> tag. Note that module scripts are deferred by default, and async makes 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 async attribute — 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 .js file and reference it with the src attribute alongside defer.
- If the script must remain inline, remove the defer attribute entirely. If you need deferred execution for inline code, consider placing the <script> element at the end of the <body>, or use DOMContentLoaded to 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>
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 the defer or async attribute 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>
The charset attribute was historically used to declare the character encoding of an external script file when it differed from the document’s encoding. In modern HTML, this attribute is obsolete because the HTML specification now requires that the character encoding of an external script must match the encoding of the document itself. Since the document’s encoding is already declared via <meta charset="UTF-8"> (or through HTTP headers), specifying it again on individual <script> elements is redundant and no longer valid.
If your external script files use a different encoding than your HTML document, the correct solution is to convert those script files to match the document’s encoding (typically UTF-8) rather than using the charset attribute. UTF-8 is the recommended encoding for all web content and is supported universally across browsers.
This matters for standards compliance — the HTML living standard explicitly marks this attribute as obsolete. It also affects maintainability, since having encoding declarations scattered across script tags can create confusion about which encoding is actually in effect. Browsers may also ignore the attribute entirely, leading to unexpected behavior if you’re relying on it.
How to fix it
- Remove the charset attribute from all <script> elements.
- Ensure your document declares its encoding using <meta charset="UTF-8"> inside the <head>.
- Convert any script files that aren’t already UTF-8 to use UTF-8 encoding. Most modern code editors can do this via “Save with Encoding” or a similar option.
- While you’re at it, you can also remove type="text/javascript" — this is the default type for scripts and is no longer needed.
Examples
Incorrect: using the obsolete charset attribute
<script src="app.js" type="text/javascript" charset="UTF-8"></script>
This triggers the validation warning because charset is obsolete. The type="text/javascript" is also unnecessary since it’s the default.
Incorrect: charset on an inline script
<script charset="UTF-8">
console.log("Hello");
</script>
The charset attribute was only ever meaningful for external scripts, and even in that context it is now obsolete.
Correct: clean script element
<script src="app.js"></script>
Correct: ensuring encoding is declared at the document level
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Page</title>
<script src="app.js"></script>
</head>
<body>
<p>Content here.</p>
</body>
</html>
The <meta charset="UTF-8"> declaration in the <head> covers the encoding for the entire document, including all linked scripts. No additional charset attributes are needed on individual elements.
The language attribute was used in early HTML to specify the scripting language of a <script> block, typically set to values like "JavaScript" or "VBScript". It was deprecated in HTML 4.01 (in favor of the type attribute) and is now fully obsolete in the HTML Living Standard. While browsers still recognize it for backward compatibility, it serves no functional purpose and triggers a validation warning.
The <script> element accepts several standard attributes, but the two most common are type and src. The type attribute specifies the MIME type or module type of the script (e.g., "module" or "application/json"), and src points to an external script file. When writing standard JavaScript, you can omit type entirely because "text/javascript" is the default. The language attribute, however, should always be removed — it is not a valid substitute for type and has no effect in modern browsers.
Why this matters
- Standards compliance: Using obsolete attributes means your HTML does not conform to the current HTML specification. This can cause validation errors that obscure more important issues in your markup.
- Code clarity: The language attribute is misleading to developers who may not realize it’s non-functional. Removing it keeps your code clean and easier to maintain.
- Future-proofing: While browsers currently tolerate the attribute, there is no guarantee they will continue to do so indefinitely. Relying on obsolete features is a maintenance risk.
How to fix it
Simply remove the language attribute from your <script> elements. If you’re using JavaScript (the vast majority of cases), no replacement is needed. If you need to specify a non-default type, use the type attribute instead.
Examples
❌ Obsolete: using the language attribute
<script language="JavaScript">
console.log("Hello, world!");
</script>
<script language="JavaScript" src="app.js"></script>
✅ Fixed: attribute removed
For inline JavaScript, simply omit the attribute:
<script>
console.log("Hello, world!");
</script>
For external scripts, only src is needed:
<script src="app.js"></script>
✅ Using the type attribute when needed
If you need to specify a script type — for example, an ES module or a data block — use the standard type attribute:
<script type="module" src="app.js"></script>
<script type="application/json">
{ "key": "value" }
</script>
Note that type="text/javascript" is valid but redundant, since JavaScript is the default. You can safely omit it for standard scripts.
The <script> element’s type attribute specifies the MIME type of the script. In earlier HTML versions (HTML 4 and XHTML), the type attribute was required and authors had to explicitly declare type="text/javascript". However, the HTML5 specification changed this — JavaScript is now the default scripting language, so when the type attribute is omitted, browsers automatically treat the script as JavaScript.
Because of this default behavior, including type="text/javascript" (or variations like type="application/javascript") is unnecessary. The W3C HTML Validator raises a warning to encourage cleaner, more concise markup. While this isn’t an error that will break your page, removing the redundant attribute keeps your HTML lean and aligned with modern standards.
There are legitimate uses for the type attribute on <script> elements, such as type="module" for ES modules or type="application/ld+json" for structured data. These values change the behavior of the <script> element and should absolutely be kept. The validator only flags the attribute when its value is a JavaScript MIME type, since that’s already the default.
Examples
Incorrect — unnecessary type attribute
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript">
console.log("Hello, world!");
</script>
Correct — type attribute removed
<script src="app.js"></script>
<script>
console.log("Hello, world!");
</script>
Correct — type attribute used for non-default purposes
The type attribute is still necessary and valid when you’re using it for something other than plain JavaScript:
<!-- ES module -->
<script type="module" src="app.mjs"></script>
<!-- JSON-LD structured data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Example Inc."
}
</script>
<!-- Import map -->
<script type="importmap">
{
"imports": {
"utils": "./utils.js"
}
}
</script>
Quick fix checklist
- Search your HTML files for type="text/javascript" and type="application/javascript".
- Remove the type attribute from those <script> tags entirely.
- Leave the type attribute on any <script> tags that use type="module", type="importmap", type="application/ld+json", or other non-JavaScript MIME types.
Ready to validate your sites?
Start your free trial today.