spectreconsole/spectre.console · error · InvalidOperationException

Unbalanced markup stack. Did you forget to close a tag?

Error message

Unbalanced markup stack. Did you forget to close a tag?

What it means

After consuming all tokens in AnsiMarkup.Parse, the style stack is non-empty, meaning one or more opening tags `[...]` were never closed with a matching `[/]`. The error prompts the developer to add the missing closer.

Source

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

                    // Merge segments with same style and link
                    result[^1].Text += token.Value;
                }
                else
                {
                    result.Add(
                        new AnsiMarkupSegment(
                        token.Value, style.Value, link));
                }
            }
            else
            {
                throw new InvalidOperationException("Encountered unknown markup token.");
            }
        }

        if (styleStack.Count > 0)
        {
            throw new InvalidOperationException("Unbalanced markup stack. Did you forget to close a tag?");
        }

        // Try resolving auto links (if any)
        ResolveAutoLinks(result);

        return result;
    }

    private static void ResolveAutoLinks(List<AnsiMarkupSegment> segments)
    {
        for (var segmentIndex = 0; segmentIndex < segments.Count; segmentIndex++)
        {
            var currentLink = segments[segmentIndex].Link;
            if (currentLink?.Url.Equals(Constants.EmptyLink, StringComparison.Ordinal) != true)
            {
                // Not a link
                continue;
            }

View on GitHub (pinned to 0acc92fada)

Solutions

  1. Add a matching `[/]` for every `[...]` opener
  2. For nested styles, close in the correct order or rely on the stack (each `[/]` pops one level)
  3. Validate tag balance before rendering, or escape dynamic text with AnsiMarkup.Escape
  4. Wrap rendering of dynamic markup in try/catch

Example fix

// before
AnsiMarkup.Parse("[red]Error occurred");

// after
AnsiMarkup.Parse("[red]Error occurred[/]");
Defensive patterns

Strategy: validation

Validate before calling

// Verify every opener has a matching closer:
public static bool HasMatchingClosers(string markup)
{
    int open = 0;
    for (int i = 0; i < markup.Length; i++)
    {
        char c = markup[i];
        if (c == '[' && i + 1 < markup.Length && markup[i + 1] == '[') { i++; continue; }
        if (c == '[') open++;
        else if (c == ']' && i > 0 && markup[i - 1] == ']') continue;
        else if (markup.Length - i >= 3 && markup.Substring(i, 3) == "[/]") open--;
    }
    return open == 0;
}

Try / catch

try { var segments = AnsiMarkup.Parse(markup); }
catch (InvalidOperationException ex) when (ex.Message.StartsWith("Unbalanced markup stack"))
{ /* append missing closers or render as plain text */ }

Prevention

When it happens

Trigger: Markup such as `"[red]text"`, `"[bold][blue]hi"`, or any input where openers outnumber closers, passed to AnsiMarkup.Parse/Write.

Common situations: Forgotten `[/]` at the end of a styled segment; nested tags where only the inner one is closed (`[red][bold]x[/]`); templating that drops trailing closers; truncation of a markup string.

Related errors


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