Introduction
Anticipation is a library to promote proactive error management over reactive exception handling.
Exception Strategy
By default, an application will have an exception strategy. That is, when something goes wrong, an Exception will be thrown and the application will either handle it or fail. Typically, this starts by wrapping functions in try/catch blocks and then handling the various exceptions that are thrown. That normally involves a process of debugging, stopping on an exception, examining the state of the object that throws the exception, and then determining how to handle it either by returning a message to the user or correcting the state.
For example, the following code attempts to call a method that should return a value. If the method fails, then an exception is caught and message returned to the user.
try
{
decimal average = AverageCalculator.Calculate(values);
Console.WriteLine($"The average is {average}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
This can be effective, but the Exception messages are not always user friendly and it requires you to react when something goes wrong.
Error Strategy
The Error strategy attempts to move more of the logic and thought into the domain objects allowing the client code to focus more on user facing concerns.
var average = AverageCalculator.Calculate(values);
if (average.Error is not null)
Console.WriteLine(average.Error.Message);
else
Console.WriteLine($"The average is {average.Value}");
Here the user code has two possible outcomes – success or failure. Both return a message to the user. The concept here is to push more logic into the method where there is a better understanding of the object state and what is good and what is not good state.
How to use Anticipation
There are many ways you can make Anticipation work for you. To put Anticipation to the best use, the method you call should return a Result type that can return either a value or an Error. In the example above, you would create an AverageResult and refactor the method to return the AverageResult after checking for Error states.
Here we define a record type to encapsulate the possible return types
public record AverageResult(decimal Value, Error Error)
Then we adjust the method to return the new AverageResult type.
public static AverageResult Calculate(int[] values)
{
decimal count = values.Length;
if(count <= 0)
return new AverageResult(0, new Error("2",""No values were provided. At least one valid value must be provided."));
decimal total = 0;
foreach (int value in values)
{
if (value > 100)
return new AverageResult(0, new Error("1","The Value provided was too high. Values must be below 100."));
if (value < 0)
return new AverageResult(0, new Error("0","The Value provided was too low. Values must be zero or higher."));
total += value;
}
decimal result = total / count;
return new AverageResult(result, null);
}
This then allows you to use the method as demonstrated in the previous Error Strategy example. An even better implementation is to use the DigitalResult package to define a discriminated union result type that can return either a value or an error. You can find out more in the product page or in the blog post about using discriminated unions for results.
Defining Errors
Errors can be predefined. It is not required, but it does make your code a bit cleaner to define and reuse the possible Errors.
public static class AverageError
{
public static Error ValueTooLow => new("0", "The Value provided was too low. Values must be zero or higher.");
public static Error ValueTooHigh => new("1","The Value provided was too high. Values must be below 100.");
public static Error NoValues => new("2","No values were provided. At least one valid value must be provided.");
}
Now, you simply reference the predefined error where ever you wish to return such an error in your code.
return new AverageResult(0, AverageError.ValueTooHigh(value));
This allows you to get more creative with your error responses. For example, it would be useful for the user to know what value they entered that caused the Error.
public static Error ValueTooLow(int value) => new("0", $"The Value provided '{value}' was too low. Values must be zero or higher.");
The example above allows you to pass the current value to the Error in order to provide a more meaningful response to the user.
Do you like what you see?
For more information, you can check out the code on GitHub. If you like it, give it a Star, consider Sponsoring us, and feel free to contribute.
Would you like to see more? Check out our blog and other products.