emberjs/ember.js · error

Closing tag </${tag.name}> without an open tag

Error message

Closing tag </${tag.name}> without an open tag

What it means

When finishEndTag validates a closing tag, if the corresponding node on the element stack is not an ElementNode, there is no matching open element — the closing tag has nothing to close.

Source

Thrown at packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts:592

      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.SourceSpan
  ): ASTv1.AttrValue {
    if (isDynamic) {
      if (isQuoted) {
        return this.assembleConcatenatedValue(parts);
      } else {

View on GitHub (pinned to 26f97246a8)

Solutions

  1. Add the missing opening tag matching the closing tag name
  2. Remove the stray closing tag
  3. Rebalance tags after a merge conflict; check nesting of `{{#if}}`/`{{#each}}` blocks around the reported line

Example fix

// before
<p>text</div>
// after
<p>text</p>
Defensive patterns

Strategy: try-catch

Validate before calling

// balance-check open/close tags before parsing
const opens = (template.match(/<\w+/g) || []).length;
const closes = (template.match(/<\/\w+>/g) || []).length;
if (closes > opens) throw new Error('More closing tags than opening tags');

Try / catch

try { parse(template) } catch (e) { if (/without an open tag/.test(e.message)) { /* remove stray closing tag or add opener, then retry parse */ } else throw e; }

Prevention

When it happens

Trigger: A `</div>` (or any `</name>`) appears when the innermost open node is text, a mustache/comment, or nothing at all — i.e. the start tag is missing or was already closed.

Common situations: Deleting an opening tag but leaving the closing one, mismatched mustache blocks consuming the element stack, merge conflicts that drop one side of a tag pair.

Related errors


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