Skip to content

Exceptions#

CommandDotNet rethrows all exceptions raised by the application code. The app developer is responsible for ensuring they are logged and displayed to the user.

CommandDotNet throws two types of exceptions

  • ValueParsingException: indicates user error and is localizable.
  • InvalidConfigurationException: indicates exceptions that can be fixed by developers. These exceptions are not a result of user error and are thus not localizable. It is expected the developer will discover these during testing.

Two patterns for handling exceptions#

UseErrorHandler deletage#

static int Main(string[] args)
{
    return new AppRunner<Program>()
        .Configure(b =>
        {
            // some other setup that could throw exceptions
            // i.e. configure containers, load configs, register custom middleware
        })
        .UseErrorHandler((ctx, ex) =>
        {
            (ctx?.Console.Error ?? Console.Error).WriteLine(ex.Message);
            return ExitCodes.Error.Result;
        })
        .Run(args);
}
snippet source | anchor

This delegate will be used for any errors thrown within AppRunner.Run or AppRunner.RunAsync

When to use#

If you plan to test your app using the TestTools approach mentioned here.

All application configuration should occur within AppRunner.Configure or other middleware extensions to ensure the configuration is executed within AppRunner.Run or AppRunner.RunAsync

This is the recommended approach.

Try/Catch AppRunner.Run#

Wrap the call to appRunner.Run(args) to handle global exceptions.

static int Main(string[] args)
{
    try
    {
        // some other setup that could throw exceptions
        // i.e. configure containers, load configs, register custom middleware

        return new AppRunner<Program>().Run(args);
    }
    catch (Exception ex)
    {
        Console.Error.WriteLine(ex.Message);
        return ExitCodes.Error.Result;
    }
}
snippet source | anchor

Exit Codes#

CommandDotnet has the following pre-defined exit codes

public static class ExitCodes
{
    public static Task<int> Success => Task.FromResult(0);
    public static Task<int> Error => Task.FromResult(1);
    public static Task<int> ValidationError => Task.FromResult(2);
}
snippet source | anchor

CommandDotNet internal errors are generally captured and return ExitCodes.Error (1).

Validation middleware will generally return ExitCodes.ValidationError (2).

When middlware causes an exception, it should print error messages and return an error code instead of raising the exception.

Additional Diagnostic Info#

You can see each in use in the example at the end.

CommandContext from the exception#

Exceptions that escape appRunner will contain the CommandContext in the Exception.Data property.

Use the ex.GetCommandContext() extension method to get the instance. Use the namespace CommandDotNet.Diagnostics

With the exception, use one of the ex.Print(...) extension methods to also print exception properties, .Data values, and stack trace.

Here are the core parameters

bool includeProperties = false,  // print exception properties
bool includeData = false,        // print values from ex.Data dictionary
bool includeStackTrace = false,  // print stack trace
bool excludeTypeName = false     // do not print exception type name
snippet source | anchor

With the context, use the ctx.PrintHelp() extension method. If the error occurred after the parser determined the target command, prints help for the target command. If a target command was not determined, prints help for the root command.

Use this for user related errors where it could be helpful for them to see the help again to change their input.

Printing config information#

Configuration can be printed with appRunner.ToString(). See the Command Logger > AppConfig section for an example.

Alternatively, CommandLogger.Log(ctx) can be used to output CommandLogger for exception handling.

This can be very helpful for troublshooting, but could also be overly verbose for command errors. Consider your user base carefully when using this pattern.

Full example#

Here's an example using all the features from above, which is likely overkill for most apps, but shown so you can see what's available.

static int Main(string[] args) => AppRunner.Run(args);

public static AppRunner AppRunner =>
    new AppRunner<Program>()
        .UseErrorHandler(ErrorHandler);

private static int ErrorHandler(CommandContext? ctx, Exception exception)
{
    var errorWriter = (ctx?.Console.Error ?? Console.Error);
    exception.Print(errorWriter.WriteLine,
        includeProperties: true,
        includeData: true,
        includeStackTrace: false);

    // use CommandLogger if it has not already logged for this CommandContext
    if (ctx is not null && !CommandLogger.HasLoggedFor(ctx))
    {
        CommandLogger.Log(ctx,
            writer: errorWriter.WriteLine,
            includeSystemInfo: true,
            includeMachineAndUser: true,
            includeAppConfig: false
        );
    }

    // print help for the target command or root command
    // if the exception occurred before a command could be parsed
    ctx?.PrintHelp();

    return ExitCodes.Error.Result;
}

public void Throw(string message)
{
    throw new ArgumentException(message, nameof(message))
    {
        Data = { { "method", nameof(Exceptions.Throw) } }
    };
}
snippet source | anchor

$ example.exe Throw yikes
System.ArgumentException: yikes (Parameter 'message')
Properties:
  Message: yikes (Parameter 'message')
  ParamName: message
Data:
  method: Throw

***************************************
Original input:
  Throw yikes

command: Throw

arguments:

  message <Text>
    value: yikes
    inputs: yikes
    default:

Tool version  = doc-examples.dll 1.1.1.1
.Net version  = .NET 5.0.13
OS version    = Microsoft Windows 10.0.12345
Machine       = my-machine
Username      = \my-machine\username
***************************************

Usage: example.exe Throw <message>

Arguments:

  message  <TEXT>
snippet source | anchor

The sections between *************************************** lines are from the CommandLogger.

Back to top