Skip to main content

Command Palette

Search for a command to run...

Results and errors handling strategies (in C#)

Published
7 min read
Results and errors handling strategies (in C#)

Overview

A consistent and robust strategy to propagate results and errors is key for both code correctness and solidity.

Talking about results propagation we cannot avoid bring up functional programming. Behavior of purely functional code is predictable cause it's comparable to a mathematical expression.

Anyway a completely functional approach is in some cases unpractical. This explains the success of multi-paradigm languages like C#. We can also state how designers included more functional constructs to its syntax in each new release of the compiler.

Functional languages provide result types to propagate result values among functions. The key point is that even in a multi-paradigm language we can write (pure or almost) functional code to get rid of the use of null and exceptions. This is achieved using result types.

Discussing result types

For example in the .NET world F# has the Option'T type that can wrap a value as Some'T or be in form the of None when empty. Types of this kind are defined monads and in our case are used to avoid the use of null to represents the absence of a scalar value. F# as being part of .NET family still has syntax for null, while other more strictly functional languages like Haskell simply lack it at all.

For completeness the Haskell equivalent of Option'T is Maybe, defined as: Maybe a = Nothing | Just a

Sequences are represented in .NET by IEnumerable<T> can be viewed as monads, since a sequence has two forms: an empty sequence and a sequence holding values.

.NET BCL lacks a type like F# Option'T, the most closest is Nullable<T> used to wrap a value type into a nullable reference type (but as you can see with a different purpose). Back to Option-like types but you can find some implementations in NuGet. For simplicity in next samples I will use result types from a library that I personally designed: SharpX.

A C# method returning Maybe<T> looks like that:

Maybe<ushort> ComputeKcals(Food[] foods) => foods.Any()
  ? Maybe.Just(foods.Sum(x => x.Kcal))
  : Maybe.Nothing<ushort>();

And here follows the code that consumes a the Maybe<T> value:

if (ComputeKcals(foods).MatchJust(out var kcals)) {
  Console.WriteLine($"Total calories: {kcals}");
} else {
  Console.WriteLine("Failed to compute calories: food list is empty");
}

It's needed to quote another useful result type borrowed from Haskell: Either<TLeft, TRight>. This type can be in form of Left<TLeft> in case of failure and in form of Right<TRight> in case of success (easy to remind).

  • TLeft can be anything like a string with an error message, an Exception or a custom data structure designed to hold failure details (let's explore this later).

  • TRight will be any value produced by the computation.

Let's refactor the previous sample using Either:

Either<string, ushort> ComputeKcals(Food[] foods) => foods.Any()
  ? Either.Right(foods.Sum(x => x.Kcal))
  : Either.Left("Failed to compute calories: food list is empty");

var result = ComputeKcals(foods);
if (result.MatchRight(out var kcals)) {
  Console.WriteLine($"Total calories: {kcals}");
} else {
  Console.WriteLine(result.FromLeft());
}

One thing that is necessary to point out now is that the use these types can dramatically reduce the need to design new exceptions in your application. As a rule of thumb as long as an error can be treated as a value you don't need to throw an exception (just return it as a result).

Exceptions as the name itself suggests should be reserved for exceptional cases, for the unexpected.

Use of Maybe and Either

You can think of Maybe type as a mean to avoid handling null values and Either as a Maybe that can hold errors data in case of a failure. In my implementation (and hopefully in many others) both types are defined as struct so they cannot be null.

I used these types from years in various open and closed source projects, developing patterns to fit specific needs.

For example when a method returns a boolean value and you want also to supply error details, you can still use Maybe. Let's say we've a method like the following one:

bool DeployFunctionApp(string resourceName)
{
  try {
    _ = _armClient.DeployResource(resourceName, Resources.FunctionAppBicep);
  } catch (ArmException ex) {
    _logger.LogCritical(ex, $"Failed {resourceName} deployment");
  }
}

This is just a variant defined in using a non-functional design. The author of the code could have relaunched the exception (inside a custom one or less), relying in a global handler. There are many ways to refactor this code in a functional way. It depends on the eventual need for logging and/or generating a more synthetic (or human friendly) error message.

We could refactor it using wrapping a tuple inside a Maybe type:

Maybe<(string, Exception)> DeployFunctionApp(string resourceName)
{
  try {
    _ = _armClient.DeployResource(resourceName, Resources.FunctionAppBicep);
  } catch (ArmException ex) {
    return Maybe.Just(($"Failed {resourceName} deployment",
      new DeployException(resourceName, Kind.FunctionApp, ex)));
  }
  return Maybe.Nothing<(string, Exception)>();
}

In this case a Maybe in form of Nothing represents a success: no errors. The consumer of this method could log the exception or relaunch it wrapped in an another one. If have to I would suggest to launch exceptions only in the topmost part of the code: be the Program class or an ASPNET Core controller.

  • In a Console application you can centralize the exception handling defining the UnhandledException event for the current application domain.

  • In an ASPNET Core application you can write a specific middleware to handle exceptions in a central point.

Passing instanced exceptions as results and throwing them in their original state will fake the stack trace. In that case I would recommend to throw a new specific exception that wraps the original one. Anyway when you treating exception as mere data structures that hold failure data the stack trace lose its relevance, since you're relying predictability of your program flow.

To improve the previous method and consolidate a pattern through all the codebase, we can consider defining a type instead of using a tuple:

readonly struct Failure {
    public string Message { get; init; }
    public Exception Exception { get; init; }
}

The refactored method will be as follows:

Maybe<Failure> DeployFunctionApp(string resourceName)
{
  try {
    _ = _armClient.DeployResource(resourceName, Resources.FunctionAppBicep);
  } catch (ArmException ex) {
    return Maybe.Just(new Failure {
      Message = $"Failed {resourceName} deployment",
      Exception = new DeployException(resourceName, Kind.FunctionApp, ex));
  }
  return Maybe.Nothing<Failure>();
}

Let say that we need to return a resource identifier from our DeployFunctionApp method. For this eventuality we can rewrite it using Either as result type:

Either<Failure, string> DeployFunctionApp(string resourceName)
{
  try {
    var resource = _armClient.DeployResource(resourceName, Resources.FunctionAppBicep);
    return Either.Right<Failure, string>(resource.Id);
  } catch (ArmException ex) {
    return Either.Left<Failure, string>(new Failure {
      Message = $"Failed {resourceName} deployment",
      Exception = new DeployException(resourceName, Kind.FunctionApp, ex));
  }
}

In synthesis the advantage of using properly designed result types are the following.

  • Predictability. A program with a flow based on results are more predictable:

    • it's easier to reason about the expected behavior of a method/function

    • the resulting code is more resilient to errors as developers are compelled to consider and handle all possible outcomes.

  • Robustness. Results are implemented as value types:

    • the validation at compile time will completely avoid any NullReferenceException at runtime.
  • Immutability. Result types promotes immutability:

    • accidental modifications are prevented

    • a consistent representation of success or failure is ensured throughout the program.

Conclusion

That being said, do we really need to avoid nulls and exceptions to the maximum extent? In first instance we technically completely can't and more we shouldn't, so the answer is no.

In C# as in other languages these are first class constructs deeply rooted into the their design. Being dogmatic and extremist is rarely a good idea and this case is not an exception (pardon the redundancy).

Anyway for reusable code or code that could be maintained by others, I strongly recommend that you to take a clear decision taking into account these suggestions:

  • select which canonical result type you want to use

  • evaluate the need to design your own custom result types

  • decide to use or avoid result types in public signatures (e.g. if you're designing an open source class library or one shared among a wide organization)

  • decide to break these functional patterns for very internal parts of code (e.g. for performance reasons or for less verbose code).

Functional is funky! (but not this joke)