swc-project/swc · warning · Error
Stray start tag "{tag_name}"
Error message
Stray start tag "{tag_name}" What it means
While in the "in body" insertion mode, a start tag for `caption`, `col`, `colgroup`, `frame`, `head`, `tbody`, `td`, `tfoot`, `th`, `thead`, or `tr` arrived (parser/mod.rs:4197-4221). These tags only make sense inside a table/frameset context; in body content the spec says: parse error, ignore the token completely — no element is created and the content is dropped from the tree.
Source
Thrown at crates/swc_html_parser/src/parser/mod.rs:4217
//
// Parse error. Ignore the token.
Token::StartTag { tag_name, .. }
if matches!(
&**tag_name,
"caption"
| "col"
| "colgroup"
| "frame"
| "head"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "tr"
) =>
{
self.errors.push(Error::new(
token_and_info.span,
ErrorKind::StrayStartTag(tag_name.clone()),
));
}
// Any other start tag
//
// Reconstruct the active formatting elements, if any.
//
// Insert an HTML element for the token.
Token::StartTag {
is_self_closing,
tag_name,
..
} => {
self.reconstruct_active_formatting_elements()?;
self.insert_html_element(token_and_info)?;
maybe_allow_self_closing!(is_self_closing, tag_name);
}View on GitHub (pinned to 5176682b65)
Solutions
- Wrap the markup in a proper `<table>` (with `<tbody>`/`<tr>` as needed) so the tags land in table context
- If copying row/cell partials, always include the table scaffolding around them
- Replace `<frame>`-based layouts (obsolete) with iframes or CSS layout
- Note the token is IGNORED — unlike most parse errors the content disappears from the tree, so fix the input rather than tolerating it
Example fix
<!-- before --> <div><tr><td>cell</td></tr></div> <!-- after --> <div><table><tbody><tr><td>cell</td></tr></tbody></table></div>
Defensive patterns
Strategy: validation
Validate before calling
// Detect table-structure tags that are not inside a <table> ancestor
fn table_tags_outside_table(src: &str) -> bool {
let mut table = 0i32;
for tag in html_tag_tokens(src) {
match tag {
Start(ref n) if n == "table" => table += 1,
End(ref n) if n == "table" => table = table.saturating_sub(1),
Start(ref n) if matches!(n.as_str(), "caption"|"col"|"colgroup"|"frame"|"head"|"tbody"|"td"|"tfoot"|"th"|"thead"|"tr") && table == 0 => return true,
_ => {}
}
}
false
} Type guard
fn is_stray_table_start_tag(e: &Error) -> bool {
matches!(
e.kind,
ErrorKind::StrayStartTag(ref t)
if matches!(&**t, "caption"|"col"|"colgroup"|"frame"|"head"|"tbody"|"td"|"tfoot"|"th"|"thead"|"tr")
)
} Try / catch
let doc = parser.parse_document()?;
if parser.take_errors().into_iter().any(is_stray_table_start_tag) {
// CRITICAL: the token was IGNORED — content is missing from the tree; reject input
return Err(ContentRejected::TableMarkupOutsideTable);
} Prevention
- Never reuse row/cell partials outside a <table> wrapper
- Remember this family drops content (token ignored), unlike most recoveries
- CI-validate built HTML for td/tr/tbody appearing without a table ancestor
When it happens
Trigger: `<div><td>cell</td></div>`, `<body><tr><td>x`, `<span><thead>` — any table-structure tag encountered with no table context. Also `<frame>` after content when the document isn't a frameset.
Common situations: Table rows copied into non-table containers; templates where a row partial is reused outside its table; legacy frameset pages parsed as normal documents; email HTML built by string concat that drops the `<table>` wrapper.
Related errors
- Start tag for "table" seen but the previous "table" is still
- Start tag "{tag_name}" seen in "table"
- failed to parse input as document fragment
- failed to parse input as document
- failed to parse input as document fragment
AI-assisted analysis of swc-project/swc@5176682b65 (2026-08-17).
Data as JSON: /api/errors/819d4cd7a3e63318.
Report an issue: GitHub.