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.SourceSpan

View on GitHub (pinned to 26f97246a8)

Solutions

  1. Delete the end tag and use the void element alone: `<br>`, `<img src=...>`
  2. If self-closing style is preferred, use `<br />` instead of `<br></br>`
  3. 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

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


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