emberjs/ember.js · error
Invalid end tag: closing tag must not have attributes
Error message
Invalid end tag: closing tag must not have attributes
What it means
HTML closing tags cannot carry attributes. While parsing attribute values, finishAttributeValue checks the current tag; if it is an EndTag (attributes were seen after </name), it throws a syntax error spanning from the tag start to the tokenizer position.
Source
Thrown at packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts:292
// the tokenizer line/column have already been advanced, correct location info
if (char === '\n') {
loc = lastPart ? lastPart.loc.getEnd() : this.currentAttr.valueSpan.getStart();
} else {
loc = loc.move(-1);
}
this.currentAttr.currentPart = b.text({ chars: char, loc: loc.collapsed() });
}
}
finishAttributeValue(): void {
this.finalizeTextPart();
let tag = this.currentTag;
let tokenizerPos = this.offset();
if (tag.type === 'EndTag') {
throw generateSyntaxError(
`Invalid end tag: closing tag must not have attributes`,
this.source.spanFor({ start: tag.start.toJSON(), end: tokenizerPos.toJSON() })
);
}
let { name, parts, start, isQuoted, isDynamic, valueSpan } = this.currentAttr;
// Just trying to be helpful with `<Hello |foo|>` rather than letting it through as an attribute
if (name.startsWith('|') && parts.length === 0 && !isQuoted && !isDynamic) {
throw generateSyntaxError(
'Invalid block parameters syntax: block parameters must be preceded by the `as` keyword',
start.until(start.move(name.length))
);
}
let value = this.assembleAttributeValue(parts, isQuoted, isDynamic, start.until(tokenizerPos));
value.loc = valueSpan.withEnd(tokenizerPos);
View on GitHub (pinned to 26f97246a8)
Solutions
- Remove all attributes (and their values) from the closing tag: </div class="x"> → </div>.
- If the attribute was intended to affect the element, move it to the opening tag: <div class="x"> ... </div>.
- Search the template for '</' followed by attribute patterns and run ember-template-lint to catch malformed tags early.
Example fix
// before <div id="main"> content </div id="main"> // after <div id="main"> content </div>
Defensive patterns
Strategy: validation
Validate before calling
// reject attributes on closing tags before compiling
function assertNoEndTagAttributes(template) {
const bad = template.match(/<\s*\/\s*[A-Za-z][^>]*\s+[\w@][^>]*>/g);
if (bad) throw new Error(`Closing tag must not have attributes: ${bad.join(', ')}`);
}
assertNoEndTagAttributes(template); Prevention
- Keep closing tags bare: only the tag name after '</'
- Place all attributes on the opening tag
- Check copy/pasted blocks for duplicated attributes on the closing tag
- Use a formatter/linter that normalizes tag structure in CI
When it happens
Trigger: Writing attributes on a closing tag: </div class="x">, </a href={{url}}>. The tokenizer encounters attribute content while this.currentTag.type === 'EndTag', and finishAttributeValue (called from MustacheStatement-driven attribute parsing) throws before building the attribute node.
Common situations: Copy/paste errors duplicating an entire element including its attributes onto the closing tag; sloppy hand edits like </div class="active">; string-generated templates that emit attributes on both open and close tags.
Related errors
- Invalid end tag: closing tag must not be self-closing
- Changing context using "../" is not supported in Glimmer
- Mixing '.' and '/' in paths is not supported in Glimmer; use
- '.' is not a supported path in Glimmer; check for a path wit
- Attempted to parse a path expression, but it was not valid.
AI-assisted analysis of emberjs/ember.js@26f97246a8 (2026-09-01).
Data as JSON: /api/errors/1bba48900940bc9d.
Report an issue: GitHub.