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
- Add a matching `[/]` for every `[...]` opener
- For nested styles, close in the correct order or rely on the stack (each `[/]` pops one level)
- Validate tag balance before rendering, or escape dynamic text with AnsiMarkup.Escape
- 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
- Always close tags you open, especially in templates
- Escape dynamic content with AnsiMarkup.Escape
- Use try/catch around rendering of assembled markup
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
- Encountered closing tag when none was expected near position
- Encountered malformed markup tag at position {pos}.
- Encountered unknown markup token.
- Encountered unescaped ']' token at position {_reader.Positio
- A link has already been set.
AI-assisted analysis of spectreconsole/spectre.console@0acc92fada (2026-08-13).
Data as JSON: /api/errors/95eed0ea1f7983b5.
Report an issue: GitHub.