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

  1. Remove all attributes (and their values) from the closing tag: </div class="x"> → </div>.
  2. If the attribute was intended to affect the element, move it to the opening tag: <div class="x"> ... </div>.
  3. 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

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


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