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

  1. Close tags in the reverse order they were opened, or reorder the nesting
  2. Match the closing tag name to the last open tag (see line number in the error)
  3. 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

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


AI-assisted analysis of emberjs/ember.js@26f97246a8 (2026-09-01). Data as JSON: /api/errors/99168c9201e7e99d. Report an issue: GitHub.