emberjs/ember.js · error
<${tag.name}> elements do not need end tags. You should remo
Error message
<${tag.name}> elements do not need end tags. You should remove it What it means
validateEndTag checks every closing tag. HTML void elements (area, base, br, col, embed, hr, img, input, link, meta, param, source, track, wbr) can never have content or an end tag, so writing `</br>` etc. is rejected even though the parser also calls EndTag for self-closing start tags.
Source
Thrown at packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts:587
let first = getFirst(parts);
let last = getLast(parts);
return b.concat({
parts,
loc: this.source.spanFor(first.loc).extend(this.source.spanFor(last.loc)),
});
}
validateEndTag(
tag: StartTag | EndTag,
element: ASTv1.ParentNode,
selfClosing: boolean
): asserts element is ASTv1.ElementNode {
if (voidMap.has(tag.name) && !selfClosing) {
// EngTag is also called by StartTag for void and self-closing tags (i.e.
// <input> or <br />, so we need to check for that here. Otherwise, we would
// throw an error for those cases.
throw generateSyntaxError(
`<${tag.name}> elements do not need end tags. You should remove it`,
tag.loc
);
} else if (element.type !== 'ElementNode') {
throw generateSyntaxError(`Closing tag </${tag.name}> without an open tag`, tag.loc);
} else if (element.tag !== tag.name) {
throw generateSyntaxError(
`Closing tag </${tag.name}> did not match last open tag <${element.tag}> (on line ${element.loc.startPosition.line})`,
tag.loc
);
}
}
assembleAttributeValue(
parts: ASTv1.AttrPart[],
isQuoted: boolean,
isDynamic: boolean,
span: src.SourceSpanView on GitHub (pinned to 26f97246a8)
Solutions
- Delete the end tag and use the void element alone: `<br>`, `<img src=...>`
- If self-closing style is preferred, use `<br />` instead of `<br></br>`
- Lint templates to catch void end tags across the codebase
Example fix
// before <br></br> // after <br>
Defensive patterns
Strategy: validation
Validate before calling
const VOID = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
for (const m of template.matchAll(/<\s*(\w+)[^>]*>\s*<\/\s*\1\s*>/g)) {
if (VOID.has(m[1])) throw new Error(`</${m[1]}> is not allowed: void element`);
} Try / catch
try { parse(template) } catch (e) { if (/do not need end tags/.test(e.message)) { /* strip the end tag and reparse */ } else throw e; } Prevention
- Never write end tags for void elements
- Prefer `<br>` or `<br />` over `<br></br>`
- Add an ember-template-lint rule for void end tags
When it happens
Trigger: Parsing a template containing an explicit end tag for a void element, e.g. `<br></br>` or `<input></input>` (not self-closing start tag form).
Common situations: Old HTML habits (`<br/>` written as `<br></br>`), copy-pasted markup from other sources, icon or spacer markup like `<img ...></img>`.
Related errors
- Closing tag </${tag.name}> without an open tag
- Closing tag </${tag.name}> did not match last open tag <${el
- Compile Error: ${error.problem} @ ${error.span.start}..${err
- Compile Error: ${template.problem} @ ${template.span.start}.
- Changing context using "../" is not supported in Glimmer
AI-assisted analysis of emberjs/ember.js@26f97246a8 (2026-09-01).
Data as JSON: /api/errors/8fff614345a5a1ff.
Report an issue: GitHub.