swc-project/swc · warning · Error

StrayEndTag

StrayEndTag

Error message

Stray end tag "{tag_name}"

What it means

In the 'before head' insertion mode (crates/swc_html_parser/src/parser/mod.rs:1647), any end tag other than head/body/html/br is a parse error (StrayEndTag) and is ignored. The parser is past <html> but has not built <head> yet, so such closers match nothing.

Source

Thrown at crates/swc_html_parser/src/parser/mod.rs:1647

                    Token::StartTag { tag_name, .. } if tag_name == "head" => {
                        let element = self.insert_html_element(token_and_info)?;

                        self.head_element_pointer = Some(element);
                        self.insertion_mode = InsertionMode::InHead;
                    }
                    // An end tag whose tag name is one of: "head", "body", "html", "br"
                    //
                    // Act as described in the "anything else" entry below.
                    Token::EndTag { tag_name, .. }
                        if matches!(&**tag_name, "head" | "body" | "html" | "br") =>
                    {
                        anything_else(self, token_and_info)?;
                    }
                    // Any other end tag
                    //
                    // Parse error. Ignore the token.
                    Token::EndTag { tag_name, .. } => {
                        self.errors.push(Error::new(
                            token_and_info.span,
                            ErrorKind::StrayEndTag(tag_name.clone()),
                        ));
                    }
                    // Anything else
                    //
                    // Insert an HTML element for a "head" start tag token with no attributes.
                    //
                    // Set the head element pointer to the newly created head element.
                    //
                    // Switch the insertion mode to "in head".
                    //
                    // Reprocess the current token.
                    _ => {
                        anything_else(self, token_and_info)?;
                    }
                }
            }

View on GitHub (pinned to 5176682b65)

Solutions

  1. Remove the stray end tag from the region between <html> and the first head/body content
  2. Let the parser auto-create head/body and place markup there
  3. Filter ErrorKind::StrayEndTag(_) if pre-head junk is tolerated

Example fix

<!-- before -->
<html>
  </div>
  <p>x</p>
</html>

<!-- after -->
<html>
  <p>x</p>
</html>
Defensive patterns

Strategy: validation

Validate before calling

fn end_tag_before_head(html: &str) -> Option<usize> {
    let lower = html.to_ascii_lowercase();
    let html_end = lower.find("<html").map(|i| i + 5)?;
    let head = lower.find("<head").unwrap_or(lower.len());
    lower[html_end..head].find("</").map(|i| html_end + i)
}

Try / catch

use swc_html_parser::error::ErrorKind;

let mut errors = Vec::new();
let doc = swc_html_parser::parse_file_as_document(&fm, config, &mut errors)?;

for err in &errors {
    if let ErrorKind::StrayEndTag(tag) = err.kind() {
        log::warn!("end tag </{}> before <head> ignored", tag);
    }
}

Prevention

When it happens

Trigger: An end tag directly following the html start tag with no head started: `<html></div>`, `<html>\n</span>text`. The four exceptional names (head/body/html/br) take the 'anything else' path that implicitly creates <head>.

Common situations: Layout templates with stray closers left in the pre-head region, trimmed partials pasted after an explicit <html>, hand-written document shells.

Related errors


AI-assisted analysis of swc-project/swc@5176682b65 (2026-08-17). Data as JSON: /api/errors/5991141f739ebf25. Report an issue: GitHub.