Breaking Free From Exceptions – A Different Way Forward

Breaking Free From Exceptions – A Different Way Forward

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!

Nick Cosentino

Principal Software Engineering Manager at Microsoft

1 年

If you enjoy this kind of content, consider following for more! You can subscribe on YouTube to get updates about my videos as soon as they're published: https://www.youtube.com/@devleader You can also check out my new 5-minute weekend read newsletter for software engineering and C# tips: https://www.devleader.ca/newsletter Thanks for your support!

回复

要查看或添加评论,请登录

Nick Cosentino的更多文章

社区洞察

其他会员也浏览了