Control Flows, expect the expected | Opinionated Pattern Picking

Welcome to Opinionated Pattern Picking (OPP), a monthly session we run at Arinco to foster an environment of discussion and learning for our application developers and anyone else interested. Each month the team discusses a topic and attempts to elect a “best default” pattern for developers to use on future projects.


What solution are we designing for?
Instead of looking to build an enterprise solution, we take the approach of starting with a straightforward solution: we create a solution that involves an API and some data that can be retrieved either in response to an end-user request or by a daemon service running in the background. This simple approach is still quite detailed, providing us with enough substance to delve into before adding further complexity.



August – Control Flows

The topic in August was control flows. Control flows can be defined in two ways, in method keywords which impact the flow of execution of your program, for example:

  • switch
  • if
  • foreach

Or method return type which indicate success or failure of a calling method and the subsequent flow of the code, for example when a method executes correctly it will return the type in its method signature, if not, it may throw an exception, hence changing the execution of the program. For the purpose of our discussion, we focused on the latter definition.



Pattern Picking


Pattern #1: Try, Catch and Exceptions


Let’s consider the use of try-catch blocks, which is present in the C# language specification. These blocks effectively manage most of the exceptional cases that might be thrown, but they can also result in returning exceptions for invalid code states. For example, if an authentication parameter is missing or invalid, an exception may be thrown to indicate that the endpoint is not being authenticated correctly.

However, an issue with exceptions is that they are not known to consumers at compile time. While you can use XML documentation comments (triple slash annotations) to indicate that an exception may occur, there is nothing at compile time that enforces or verifies this information. Anyone could forget to document an exception or even provide inaccurate information, so there is no compile-time safety. You just to have to be mindful and check it yourself.

As we can see above when validating whether a denominator is zero could be seen as both an exceptional case and a form of validation. While this might be required when creating a NuGet package and we need to program defensively, we are designing for a system that will only be interacting with code internal to the system, so this isn’t required. As such, it was agreed that using exceptions as a return type to impact control flow, was a bit heavy handed. however, that returning an exception for exceptional circumstances was an encouraged control flow pattern, when the problem warranted a system interrupt e.g running out of disk space.

Note regarding global exception handlers that will be important later. Throughout the discussion, it was established that pattern often used by the team was a global exception handler to handle the thrown exceptions of the system. This approach is essentially a catch-all scenario, where exceptions are caught and handled at the root of the application.

Pattern #2: Result Types


The result type pattern acts essentially as a wrapper around the return type of a method. For instance, in the example of dividing two numbers, the return type is a “double”, but it could be any object. The result type is generic and can be defined for any type in the method’s type definition.

The result pattern also allows for the addition of failure strings through methods like “Result.Fail()“. You can include one or multiple failure messages within the object if an error occurs.

When you return a result, the handling is clearer. For example, we have methods like `HandleResult` that can determine if the result was a success and proceed accordingly, or handle errors based on the failure strings added to the result object. This means that each error message included in the result object can be processed and output as needed.

With the result type, we have a clearly defined return type: if it’s a “happy path,” we proceed with the “double” value as usual. If it’s not, we have a failure type that can be handled programmatically, providing a more explicit methods `result.IsSuccess`  and `result.Errors` for the consuming methods to use. As opposed to relying solely on `try-catch` blocks and hoping for the best.

Pattern #3: Discriminated unions


The discriminated unions approach allows you to handle all the return types of a method, but it comes with a trade-off that it requires you to manage all of these return types, meaning that the code cannot be compiled until each result is properly handled. For example, if there are 15 return types, each must be resolved every time the object is handled. This ensures that, broadly speaking, all potential return types, states, and results of the method are handled within the code.

With all return types handled there is a higher degree of certainty that the code execute without exception. The compiler enforces this by using a type T object method signature with multiple types, such as a combination of a double and a string (seen above). To control the flow of execution, there is a switch method that requires a function for each declared type. At run time, this method switches between states and executes the function that corresponds to the specific state encountered.

During the discussion there were issues with the suggested NuGet package that implemented the discriminated unions pattern.

  1. The matched type in the switch case are not strongly defined, it is simple in order of the return type definition. Making it possible to mix up the order of handling return types. For example, if there are ten return types, they could be handled in the wrong order, and the compiler would not detect this until a runtime error occurs. For example, the below would fail at runtime, because success would be the error string and a string cannot be multiplied.

2. There is another issue is to do with the IntelliSense which is that instead of the package exposing a type property the package exposes a generic set of T1 and T2…. Tn objects. This can make it challenging to determine the exact type of the object you need without the method return signature for context.

While these downsides do impact the viability of discriminated unions within a project, it is worth noting that there is an issue on github, tracking the native implementation of discriminated unions in C#, which could be worth revisiting when completed.



Opiniated Pattern Picked

Once the patterns were discussed the team voted for the pattern that best declared all return states of a method while still being developer friendly. The result pattern won by an overwhelming majority. This was due to a few reasons:

  • The simplicity of implementation vs OneOf, primarily in regards knowing types during coding and the switch pattern order.
  • No magic exceptions are being thrown for what could be considered a valid code state, just not an ideal state.
  • Strongly typed return types for 99% of control flow state.

A Note on exceptions:

It is important to note that exceptions should still be thrown, however, an exceptional case has to be determined to for it to be thrown. While the exact nature of what conditions need to be met to warrant throwing an exception, it was felt that there should still be a clear note, that it would be allowed. Additionally, it was discussed that to handle these exceptions a global exception handler would be implemented but try-catch blocks in the main body of code would be excluded.

The final amendment was that the result pattern would primarily be used for business logic within a domain layer, and its consuming methods and services. The exposing API wouldn’t expose this type to the calling user or service, and data layers wouldn’t return a result either, as they should primarily just return their data.

So, to summarise:

  • Fluent results in domain logic
  • Exceptions for exceptional errors

Read more recent blogs

Get started on the right path to cloud success today. Our Crew are standing by to answer your questions and get you up and running.