Exception handling is a critical aspect of software development, and C# provides powerful mechanisms to handle and propagate exceptions. However, what if there was an alternative approach that allows us to return exceptions instead of throwing them? Is there a better way than try, catch, throw, handle, maybe rethrow, etc… Can we find an alternative way?
This post can be read in full on my blog!
I’ve always found myself dealing with unstructured data as input in the applications I’ve had to build. Whether that’s handling user input, or writing complex digital forensics software to recover deleted information for law enforcement. I wanted to take a crack a solving this with some of the things I had dabbled with regarding implicit conversion operators.
I created a class called TriedEx<T>
, that can represent either a value or an exception, providing more control over error handling and control flow. In this blog post, we’ll explore how I use TriedEx<T>
and how it helps me with cleaning up my code while offering more failure details than a simple boolean return value for pass or fail. It might not be your cup of tea, but that’s okay! It’s just another perspective that you can include in your travels.
Implicit Operator Usage
Before diving into the specifics of TriedEx<T>
, let’s recap the concept of implicit operators. In the previous blog post, we explored how implicit operators allow for seamless conversion between different types. This concept also applies to TriedEx<T>
. By defining implicit operators, we can effortlessly convert between TriedEx<T>
, T
, and Exception
types. This flexibility allows us to work with TriedEx<T>
instances as if they were the underlying values or exceptions themselves, simplifying our code and improving readability.
And the best part? If we wanted to deal with exceptional cases, we could avoid throwing exceptions altogether.
You can check out this video for more details about implicit operators as well:
Pattern Matching with Match and MatchAsync
A key feature of TriedEx<T>
is the ability to perform pattern matching using the Match
and MatchAsync
methods. These methods enable concise and expressive handling of success and failure cases, based on the status of the TriedEx<T>
instance. Let’s look at some examples to see how this works. In the following example, we’ll assume that we have a method called Divide
that will be able to handle exceptional cases for us. And before you say, “Wait, I thought we wanted to avoid throwing exceptions!”, I’m just illustrating some of the functionality of TriedEx<T>
to start:
TriedEx<int\> result = Divide(10, 0);
result.Match(
success => Console.WriteLine($"Result: {success}"),
failure => Console.WriteLine($"Error: {failure.Message}")
);
In the above example, the Match
method takes two lambda expressions: one for the success case (where the value is available) and one for the failure case (where the exception is present). Depending on the state of the TriedEx<int>
instance, the corresponding lambda expression will be executed, allowing us to handle the outcome of the operation gracefully.
If you’d like to be able to return a value after handling the success or error case, there are overrides for that as well:
TriedEx<int\> result = Divide(10, 0);
var messageToPrint = result.Match(
success => $"Result: {success}"),
failure => $"Error: {failure.Message}")
);
Console.WriteLine(messageToPrint);
Similarly, the MatchAsync
method provides the same functionality but allows us to work with asynchronous operations. This is particularly useful when dealing with I/O operations or remote calls that may take some time to complete.
Deconstructor Usage
Another feature of TriedEx<T>
is its support for deconstruction. Deconstruction enables us to extract the success status, value, and error from a TriedEx<T>
instance in a convenient and readable way. Let’s take a look at an example:
TriedEx<string\> result = ProcessInput(userInput);
var (success, value, error) = result;
if (success)
{
Console.WriteLine($"Processed value: {value}");
}
else
{
Console.WriteLine($"Error occurred: {error.Message}");
}
In this example, the deconstruction pattern is used to unpack the success status, value, and error from the TriedEx<string>
instance. By leveraging the deconstruction syntax, we can access these components and perform the appropriate actions based on the outcome of the operation.
Practical Example: Parsing User Input
If you’re interested in seeing practical examples of how this class can be used, check out the full blog post! More code examples are included to walk you through how this design has helped me clean up my exception-handling code.
Remember to subscribe to my weekly newsletter for a quick 5-minute read every weekend! This includes content summaries, other learning resources, and community spotlights as well. Thank you for your support!
Top comments (0)