spectreconsole/spectre.console · error · InvalidOperationException

Encountered closing tag when none was expected near position

Error message

Encountered closing tag when none was expected near position {token.Position}.

What it means

In AnsiMarkup.Parse the tokenizer yielded a Close token (a `[/]`) but the style stack is empty, meaning there is no matching opening tag to close. The position reported is the token's position in the source markup.

Source

Thrown at src/Spectre.Console.Ansi/AnsiMarkup.cs:77

        var link = default(Link?);

        while (tokenizer.MoveNext())
        {
            var token = tokenizer.Current;

            if (token.Kind == MarkupTokenKind.Open)
            {
                var parsed = AnsiMarkupTagParser.Parse(token.Value);
                linkStack.Push(link);
                link = parsed.Link ?? link;
                styleStack.Push(style.Value);
                style = style.Value.Combine(parsed.Style);
            }
            else if (token.Kind == MarkupTokenKind.Close)
            {
                if (styleStack.Count == 0)
                {
                    throw new InvalidOperationException(
                        $"Encountered closing tag when none was expected near position {token.Position}.");
                }

                style = styleStack.Pop();
                link = linkStack.Pop();
            }
            else if (token.Kind == MarkupTokenKind.Text)
            {
                if (result.Count > 0 && result[^1].Style.Equals(style) && Equals(result[^1].Link, link))
                {
                    // Merge segments with same style and link
                    result[^1].Text += token.Value;
                }
                else
                {
                    result.Add(
                        new AnsiMarkupSegment(
                        token.Value, style.Value, link));

View on GitHub (pinned to 0acc92fada)

Solutions

  1. Ensure every `[/]` has a preceding matching `[...]` opener
  2. Escape literal brackets in dynamic text with AnsiMarkup.Escape(text) before splicing into markup
  3. Sanitize user input: strip or escape stray `[/]` sequences
  4. Use try/catch around AnsiMarkup.Parse when rendering untrusted markup

Example fix

// before (stray closing tag)
AnsiMarkup.Parse("Hello[/] world");

// after
AnsiMarkup.Parse("Hello world");
// or, for dynamic text, escape it first:
AnsiMarkup.Parse(AnsiMarkup.Escape(userText));
Defensive patterns

Strategy: validation

Validate before calling

// Check that openers and closers balance before parsing:
public static bool IsMarkupBalanced(string markup)
{
    int depth = 0;
    for (int i = 0; i < markup.Length; i++)
    {
        if (markup[i] == '[' && i + 1 < markup.Length && markup[i + 1] != '[') depth++;
        else if (markup[i] == ']' && i + 1 < markup.Length && markup[i + 1] == ']') i++; // escaped
        else if (markup[i] == ']' && i > 0 && markup[i - 1] == ']') { /* already handled */ }
    }
    return depth == 0;
}

Try / catch

try { var segments = AnsiMarkup.Parse(markup); }
catch (InvalidOperationException ex) when (ex.Message.StartsWith("Encountered closing tag"))
{ /* log and fall back to plain text */ }

Prevention

When it happens

Trigger: Markup like `"text[/]"`, `"[/]text"`, or any string containing more `[/]` closers than `[...]` openers, passed to AnsiMarkup.Parse or AnsiMarkup.Write.

Common situations: Concatenating markup fragments and losing an opening tag; copy-pasting only the closing half of a styled span; user input containing literal `[/]`; a templating layer that strips `[tag]` but leaves `[/]`.

Related errors


AI-assisted analysis of spectreconsole/spectre.console@0acc92fada (2026-08-13). Data as JSON: /api/errors/99c32ebf85f7ee0e. Report an issue: GitHub.