swc-project/swc · warning · Error

UnclosedElements

UnclosedElements

Error message

End tag "{tag_name}" seen, but there were open elements

What it means

The in-head template close path (crates/swc_html_parser/src/parser/mod.rs:1931): </template> found a template element on the stack, but after generate_implied_end_tags_thoroughly() the current node was still not the template — explicit elements opened inside <template> were never closed. The parser records UnclosedElements, pops everything down to and including the template, clears active formatting elements to the last marker, pops the template insertion mode, and resets the insertion mode.

Source

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

                    //
                    // Clear the list of active formatting elements up to the last marker.
                    // Pop the current template insertion mode off the stack of template insertion
                    // modes.
                    //
                    // Reset the insertion mode appropriately.
                    Token::EndTag { tag_name, .. } if tag_name == "template" => {
                        if !self.open_elements_stack.contains_template_element() {
                            self.errors.push(Error::new(
                                token_and_info.span,
                                ErrorKind::StrayEndTag(tag_name.clone()),
                            ));
                        } else {
                            self.open_elements_stack
                                .generate_implied_end_tags_thoroughly();

                            match self.open_elements_stack.items.last() {
                                Some(node) if !is_html_element!(node, "template") => {
                                    self.errors.push(Error::new(
                                        token_and_info.span,
                                        ErrorKind::UnclosedElements(tag_name.clone()),
                                    ));
                                }
                                _ => {}
                            }

                            let popped = self
                                .open_elements_stack
                                .pop_until_tag_name_popped(&["template"]);

                            self.update_end_tag_span(popped.as_ref(), token_and_info.span);
                            self.active_formatting_elements.clear_to_last_marker();
                            self.template_insertion_mode_stack.pop();
                            self.reset_insertion_mode();
                        }
                    }
                    // A start tag whose tag name is "head"

View on GitHub (pinned to 5176682b65)

Solutions

  1. Close every element inside <template> before its </template>
  2. If the content is really a reusable fragment, prefer parse_file_as_document_fragment over template wrappers
  3. Filter ErrorKind::UnclosedElements(_) when the implicit-close recovery is acceptable

Example fix

<!-- before -->
<template><div class="row">cell</template>

<!-- after -->
<template><div class="row">cell</div></template>
Defensive patterns

Strategy: validation

Validate before calling

fn template_content_is_balanced(html: &str) -> bool {
    let lower = html.to_ascii_lowercase();
    let Some(open) = lower.find("<template") else { return true };
    let Some(rel) = lower[open..].find("</template") else { return true };
    let content = &lower[open..open + rel];
    const VOID: &[&str] = &["br","img","meta","input","hr","link","source","area","base","col","embed","param","track","wbr"];
    let mut stack: Vec<String> = Vec::new();
    for (i, _) in content.match_indices('<') {
        let rest = &content[i + 1..];
        let (closing, body) = match rest.strip_prefix('/') {
            Some(b) => (true, b),
            None => (false, rest),
        };
        let name: String = body.chars().take_while(char::is_ascii_alphanumeric).collect();
        if name.is_empty() { continue; }
        if closing {
            if stack.last().map(String::as_str) != Some(&name) { return false; }
            stack.pop();
        } else if !VOID.contains(&name.as_str()) {
            stack.push(name);
        }
    }
    stack.is_empty()
}

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::UnclosedElements(tag) = err.kind() {
        // Children were implicitly closed when </template> popped the stack.
        log::warn!("</{}> implied-close of unclosed children", tag);
    }
}

Prevention

When it happens

Trigger: `<template><div class="row">cell</template>` — non-void, non-implied elements (div, span, table...) left open when the template closes. Implied-end-tag elements (li, dd, dt, p, option...) are generated automatically and do NOT trigger this; the error is only about explicit containers left dangling.

Common situations: Declarative-template content authored like text snippets, minifiers that drop closers inside template content believing them optional, hand-written HTML templates with typo'd closers.

Related errors


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