rust-lang/mdBook · critical
internal error: expected empty tag stack. path: `{}` node=
Error message
internal error: expected empty tag stack.
path: `{}`
node={node:?} What it means
The sibling case of the finish_stack drain: when the leftover item on the tag_stack is not an Element but some other node kind, mdbook-html cannot even attribute it to an unclosed HTML tag, so it panics immediately with the node debug dump. This signals deep inconsistency between the events processed and the tree structure built.
Source
Thrown at crates/mdbook-html/src/html/tree.rs:829
Node::Fragment => {}
Node::Element(el) => {
if el.was_raw {
warn!(
"unclosed HTML tag `<{}>` found in `{}`",
el.name.local,
self.options.path.display()
);
} else {
panic!(
"internal error: expected empty tag stack.\n
path: `{}`\n\
element={el:?}",
self.options.path.display()
);
}
}
node => {
panic!(
"internal error: expected empty tag stack.\n
path: `{}`\n\
node={node:?}",
self.options.path.display()
);
}
}
}
}
/// Appends a new footnote reference.
fn footnote_reference(&mut self, name: CowStr<'event>) {
let len = self.footnote_numbers.len() + 1;
let (n, count) = self
.footnote_numbers
.entry(name.clone())
.or_insert((len, 0));
*count += 1;View on GitHub (pinned to dc21064fc2)
Solutions
- Disable preprocessors one by one to find which one produces events breaking the tree invariant.
- Fix the preprocessor to emit strictly balanced, well-ordered pulldown-cmark events.
- Check for version mismatches between pulldown-cmark used by mdbook-html and your toolchain; rebuild consistently.
- Minimize the failing chapter and file a bug with the node dump from the panic message.
Example fix
// before (preprocessor) — emits End without Start events.push(Event::End(TagEnd::HtmlBlock)); // after — always pair Start/End events.push(Event::Start(Tag::HtmlBlock)); events.push(Event::Html(text)); events.push(Event::End(TagEnd::HtmlBlock));
Defensive patterns
Strategy: validation
Validate before calling
// ensure preprocessor output events are strictly balanced Start/End pairs
let mut depth = 0i32;
for ev in &processed {
match ev {
Event::Start(_) => depth += 1,
Event::End(_) => { depth -= 1; if depth < 0 { bail!("End without Start in preprocessor output"); } }
_ => {}
}
}
if depth != 0 { bail!("{} Start events without matching End", depth); } Try / catch
std::panic::catch_unwind(|| render_book(&book))
.map_err(|_| anyhow!("tag stack invariant violated: bisect preprocessors to find the one emitting malformed events"))?; Prevention
- Test preprocessors on a minimal book before enabling book-wide.
- Never emit End events without a matching Start in preprocessors.
- Upgrade mdbook/pulldown-cmark together, not independently.
When it happens
Trigger: process_events finishes a document with a non-element node (e.g. a Text/Html pseudo-entry) still on the tag stack — typically caused by a preprocessor emitting events that mutate the stack shape (append without matching pop) or an internal bug in event handling for a specific node type.
Common situations: Custom preprocessors that emit events out of contract (e.g. raw tree manipulation or misordered Start/End pairs), parser upgrades changing event kinds so tree.rs pushes/pops differently, or corrupted/generated markdown with interleaved constructs.
Related errors
- `{}` unexpected Html event: {html}
- internal error: expected `{lookup_key:?}` to be in root map
- invalid key `{name}`
- The BookBuilder should always create a valid book. If you ar
- pop too far processing `{}`
AI-assisted analysis of rust-lang/mdBook@dc21064fc2 (2026-09-01).
Data as JSON: /api/errors/0014fbb4d9176df8.
Report an issue: GitHub.