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

  1. Disable preprocessors one by one to find which one produces events breaking the tree invariant.
  2. Fix the preprocessor to emit strictly balanced, well-ordered pulldown-cmark events.
  3. Check for version mismatches between pulldown-cmark used by mdbook-html and your toolchain; rebuild consistently.
  4. 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

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


AI-assisted analysis of rust-lang/mdBook@dc21064fc2 (2026-09-01). Data as JSON: /api/errors/0014fbb4d9176df8. Report an issue: GitHub.