Handling Errors and Exit Codes

How Spectre.Console.Cli deals with exceptions and how to customize error handling

By default, Spectre.Console.Cli catches exceptions, displays a user-friendly message, and returns exit code -1. When you need more control—different exit codes for different error types, custom formatting, or integration with logging—you have two options: SetExceptionHandler for centralized handling within the framework, or PropagateExceptions for full manual control with try-catch blocks.

What We're Building

A file processor returning specific exit codes—scripts can distinguish "file not found" (exit 3) from general errors (exit 1):

Error handling and exit codes demonstration

Centralize Error Handling with SetExceptionHandler

For most applications, SetExceptionHandler provides the cleanest approach. It intercepts exceptions from both the parsing phase and command execution, letting you format errors consistently and return specific exit codes.

var app = new CommandApp<ProcessCommand>();
  
app.Configure(config =>
{
    config.SetExceptionHandler((ex, resolver) =>
    {
        AnsiConsole.WriteException(ex, ExceptionFormats.ShortenPaths);
  
        // Return specific exit codes based on exception type
        return ex switch
        {
            InvalidOperationException => 2,
            FileNotFoundException => 3,
            _ => 1
        };
    });
});
  
return app.Run(args);

The handler receives the exception and an ITypeResolver (which is null during parsing, before the command is resolved). Use pattern matching to return different exit codes based on exception type—automation scripts can then distinguish between "file not found" (exit 3), "invalid operation" (exit 2), and general errors (exit 1).

Use PropagateExceptions for Manual Control

When you need full control over exception flow—perhaps to integrate with a logging framework, perform cleanup, or handle exceptions at different layers—use PropagateExceptions(). This re-throws exceptions from app.Run(), letting you catch them in your own try-catch block.

public class PropagateExceptionsDemo
{
    public static int Run(string[] args)
    {
        var app = new CommandApp<ProcessCommand>();
        app.Configure(config => config.PropagateExceptions());
  
        try
        {
            return app.Run(args);
        }
        catch (FileNotFoundException ex)
        {
            AnsiConsole.MarkupLine($"[red]File not found:[/] {ex.FileName}");
            return 3;
        }
        catch (InvalidOperationException ex)
        {
            AnsiConsole.MarkupLine($"[red]Invalid operation:[/] {ex.Message}");
            return 2;
        }
        catch (Exception ex)
        {
            AnsiConsole.WriteException(ex);
            return 1;
        }
    }
}

This approach requires more code but gives you complete flexibility. You can catch specific exception types, access their properties for detailed messages, and integrate with any error reporting system.

Important

The exception handler configured via SetExceptionHandler will not be called when PropagateExceptions() is set—exceptions go straight to your catch blocks.

See Also