emberjs/ember.js · error
Closing tag </${tag.name}> did not match last open tag <${el
Error message
Closing tag </${tag.name}> did not match last open tag <${element.tag}> (on line ${element.loc.startPosition.line}) What it means
validateEndTag compares the closing tag name to the tag of the innermost open element. If they differ, the error reports both names and the line where the open tag started, pinpointing the actual mismatch.
Source
Thrown at packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts:594
}
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.SourceSpan
): ASTv1.AttrValue {
if (isDynamic) {
if (isQuoted) {
return this.assembleConcatenatedValue(parts);
} else {
assertPresentArray(parts);
View on GitHub (pinned to 26f97246a8)
Solutions
- Close tags in the reverse order they were opened, or reorder the nesting
- Match the closing tag name to the last open tag (see line number in the error)
- Reformat the template so element nesting is visually obvious
Example fix
// before <div><span></div></span> // after <div><span></span></div>
Defensive patterns
Strategy: try-catch
Validate before calling
// stack-based nesting check
const stack = [];
for (const m of template.matchAll(/<(\/)?(\w+)/g)) {
if (m[1]) { if (stack.pop() !== m[2]) throw new Error(`mismatched </${m[2]}>`); }
else stack.push(m[2]);
} Try / catch
try { parse(template) } catch (e) { if (/did not match last open tag/.test(e.message)) { /* jump to reported open-tag line and fix nesting */ } else throw e; } Prevention
- Close elements in LIFO order
- Indent nested elements consistently
- Let the editor auto-generate closing tags
When it happens
Trigger: `</span>` closing while `<div>` is the last open element (nesting crossed), e.g. `<div><span></div></span>`.
Common situations: Incorrectly nested inline/block elements, JSX-to-HBS conversions, hand-edits that reordered close tags, editor auto-complete closing the wrong element.
Related errors
- <${tag.name}> elements do not need end tags. You should remo
- Closing tag </${tag.name}> without an open tag
- 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/99168c9201e7e99d.
Report an issue: GitHub.