2

C#'s primary error handling mechanism are exceptions and try pattern.

We don't have access to discriminated unions yet like in case of F# and Rust Option<T> and Result<T, E> types.

The official Microsoft guidelines mention that exceptions should be avoided for errors that can happen often.

The try pattern works only for non async methods. So it's not really suitable as most methods in business logic code do some I/O operations and need to be async.

For a single point of failure returning null might make sense but in many cases there is more than 1 point of failure.

Pros of exceptions

  • Can be handled globally in a single place
  • The code doesn't need to check error status all the time and most importantly you don't forget to check it (unless you manage to make compile-time checks somehow)

Cons of exceptions

  • Performance overhead
  • You don't know what exceptions certain method throws if it's not documented and without looking at the implementation (not explicit)

Considerations and code samples

Imagine a method named AcceptPayment which can have more than one points of failure e.g. InsufficientFunds, UserAccountBlocked etc.

What would be the best way to implement custom exceptions for such cases? There are 2 perhaps more different ways

1. Exception per method with Error enum

public enum AcceptPaymentError 
{
    InsufficientFunds,
    UserAccountBlocked
}

public class AcceptPaymentException : BusinessLogicException
{
    public AcceptPaymentException(AcceptPaymentError error) : base(...)
    {
        Error = error;
    }

    public AcceptPaymentError Error { get; }
}

or just using pure string instead of enum... but then there's no way easily check what the problem was.

2 Exception per point of failure

In this case we will have InsufficientFundsException and UserAccountBlockedException deriving directly from BusinessLogicException so it can be used in more than 1 method.

Avoiding exceptions completely and using something temporarily in place of official discriminated union support

This is something I see many developers do nowadays. They don't use exceptions at all in their core/business logic layer but rely on custom types.

We can either use record and switch expressions or rely on some open source libraries providing partial support for DUs e.g.

What is your view on this subject? Would you use something like OneOf in place of exceptions when starting a new project? Why?

https://mattwarren.org/2016/12/20/Why-Exceptions-should-be-Exceptional/

5
  • 2
    The try pattern works only for non async methods. Where did you get this idea? You can't write async Task<bool> TryDoSomething() ?
    – John Wu
    Sep 20 '21 at 21:36
  • @JohnWu the try pattern involves using out parameter. And you can't have async method with out or ref parameters. stackoverflow.com/questions/18716928/…
    – Konrad
    Sep 20 '21 at 21:53
  • 2
    @Konrad, the try pattern can be implemented via an out parameter, but it doesn't have to be. You can use "poor man's maybe<T>" by having the return be T? where null indicates none. Or there are plenty of 3rd party libraries that can provide you with Maybe<T>/Option<T>. So you can use these for async via Task<T?> or Task<Maybe<T>>
    – David Arno
    Sep 21 '21 at 8:06
  • When you evaluate a new tool or technique, ask yourself the questions "what benefits does this tool or technique provide," and "do the benefits outweigh the additional costs?" Don't just adopt a technique because it's the new hotness of the week. Sep 21 '21 at 14:11
  • Regarding your cons: Performance is not a consideration unless you're throwing exceptions repeatedly in a tight loop, something you shouldn't be doing anyway (that's what the "try pattern" is for). You don't know what exceptions... It doesn't matter. All exceptions in C# derive from the Exception class; catch that, if you're worried about rogue exceptions. Sep 21 '21 at 14:14
5

Exceptions are the way to handle errors.

What people mean when they say "don't use exceptions", is "don't use exceptions to handle control flow"

So its fine for your payment handler to throw NotEnoughFunds or AccountBlocked, as these presumably hardly ever happen and the action to take is the same. Stop doing stuff and show an error. But probably not PinEnteredIncorrectly which might happen all the time and the action is prompt user to reenter three times before blocking the card

Or rather, still throw it, but add a precheck bool IsPinValid(pin) method so that you aren't relying throwing and checking an exception just to change your UI.

Exceptions catch the things you haven't thought of and can't handle, for everything else there is if

7
  • 3
    This is where I'm confused. In theory or in practice, is NotEnoughFunds really that different from PinEnteredIncorrectly? They're both a type of user error, they're both relatively predictable outcomes, they're both not system failures, they're both things that can be handled in the flow of the interaction. Maybe you're just better with money than I am, but I don't consider NotEnoughFunds to be exceptionally exceptional in a way that PinEnteredIncorrectly is not.
    – catfood
    Sep 21 '21 at 14:27
  • 1
    well there isnt enough detail in the example to say for sure. but ive tried to elaborate it. If you are looking at the type of exception and doing different things depending on it, then dont do that.
    – Ewan
    Sep 21 '21 at 14:31
  • What about something like UserNotFound which might happen pretty often when searching for some user. To me they are all errors that can happen often depending on what someone is trying to do. Can you elaborate on "don't use exceptions to handle control flow"? I think they are and always were a part of control flow in C#.
    – Konrad
    Sep 21 '21 at 23:34
  • You are trying to use some library. It can throw SomeLibraryException and you have to handle it. It's part of your control flow. Because if the call depends on the user input it's prone to fail and it should be designed in a way to handle frequent failures without worrying about possible future performance bottlenecks. And I read some benchmarks on some blog that exceptions can be 30.000 times slower. If I expect something to fail often then it's not really exceptional no?
    – Konrad
    Sep 21 '21 at 23:40
  • I think this especially matters for real-time systems.
    – Konrad
    Sep 21 '21 at 23:47
2

@ewan's answer is spot on, but I wanted to express this from the performance angle for you.

The official Microsoft guidelines mention that exceptions should be avoided for errors that can happen often.

I believe the main reason for that advice is indeed for performance. If you follow YAGNI/KISS and other principles, it really doesn't matter, because you should be optimising performance later anyway.

The reason why you might use an exception over a boolean, might be because you have a very strong "happy path". Validation is a good example, and I'll give you one but without transaction and rollback.

A first version could be:

void BatchProcessInsuranceClaims(List<InsuranceClaim> claims)
{
    foreach (var item in claims)
    {
        try
        {
            ValidateClaim(item);
            LodgeClaim(item);
            NotifyUserOfSuccess(item);
            MarkClaimAsProcessed(item);
        }
        catch (System.Exception e)
        {     
            RecordClaimError(item, e);
        }
    }
}

This is in a loop, and is simple and very readable. Now let's pretend this business is starting to crank along there are millions processed per second, and this insurance company is knocking back many more claims to make more money. This try-catch has been identified as a bottleneck:

void BatchProcessInsuranceClaims(List<InsuranceClaim> claims)
{
    foreach (var item in claims)
    {
        try
        {
            var ValidationResult = CheckIsValidClaim(item);
            if (ValidationResult.IsError)
            {
                RecordClaimError(item, ValidationResult);
                continue;    
            }

            LodgeClaim(item);
            NotifyUserOfSuccess(item);
            MarkClaimAsProcessed(item);
        }
        catch (System.Exception e)
        {     
            //Other errors
            RecordClaimError(item, e);
        }
    }
}

So technically they should NOT be "avoided". They should be "optimised" away when the time is right.

1

This is one of those things where the original intention of the advice is often twisted and misinterpreted through blind preaching, overzealousness or misunderstanding the problem domain.

C#'s primary error handling mechanism are exceptions and try pattern.

I want to distinguish between errors and exceptions here. Errors are much broader. Exceptions are a form of error, but not all errors manifest as an exception. A very simple example here is a business validation error, which should not (and commonly don't) manifest as thrown exceptions.


General advice

The general advice is to avoid what is referred to as "flow by exception". However, this is often understood to mean "don't use exceptions", which is not a correct interpretation.

First, let's address why exceptions are viewed negatively here.

Exceptions are a C# feature which trigger the collection of a lot of data which is generally helpful to a developer during debugging, the main example here being the stacktrace of where the exception occurred. Because of this, exceptions can be relatively large, nested, intricate objects containing a lot of information.

If you don't know that, you'd say that these two examples are equivalent:

try 
{
    Foo value = myFooService.FindFoo("abc");
    Console.WriteLine("foo can be found");
}
catch(FooNotFoundException ex)
{
    Console.WriteLine("foo cannot be found");

}

versus

var fooResult = myFooService.FindFoo("abc");

if(fooResult.WasFound)
    Console.WriteLine("foo can be found");
else
    Console.WriteLine("foo cannot be found");
    

However, due to the heavy load that exceptions entail, it is much faster to use a result object (second example) instead of relying on the exception-throwing feature of C#.


So when should we use exceptions?

At the risk of sounding facetious, when you encounter an exceptional circumstance.

For example, if it is reasonably expected that you sometimes will not find the foo you're looking for (in the above example), then you should account for that possibility.
If, however, a foo not being available is a very exceptional circumstance that means something has gone seriously and unexpectedly wrong, then raising an exception is perfectly fine.

Exceptions are also used as a way to back out of a function without delivering the intended result. However, it should only be used as a last resort. If a better alternative is readily available, it should be used.

A straightforward example here is division by zero. When you consider the following method:

double Divide(double a, double b)
{
   // ...
}

When b is 0, there is no way for the "normal" flow of things (= returning a number) to resume. You cannot return any double value, as this would wrongly indicate that whatever value you returned was the correct result from the division. Therefore, the only way to resolve this unresolvable situation is to forcefully break out of the normal flow which expects you to return a number.

But as a counterexample to this, consider String.IndexOf(). The "normal" return value is a number indicating the position of the first occurrence. Since this is an array index, this value is a positive integer.

When IndexOf can't find an occurrence, it returns -1 (a negative integer) to signal that it couldn't find anything. It doesn't throw an exception.

The reason that it doesn't throw an exception is that it was able to use the normal flow (returning an int) in a way that clearly indicates that something went wrong (negative indexes make no sense to indicate a position in a string).

If the designers of IndexOf had decided to make the return type an uint (positive integer), then this trick of returning -1 would not have worked. Similarly, if negative indexes had a meaningful value in C#, returning -1 wouldn't indicate that no occurrence was found.


Conclusion

The conclusion here is that exceptions are only justified when:

  • You must break out of the normal flow
  • Or the problem is such a rare occurrence that it is not worth redesigning the normal flow to account for this problem occurring. Here, you are making an active decision that the performance hit of throwing an catching an exception is irrelevant in the context of the problem that has just arisen unexpectedly.

In other words, any reasonably expected, frequent, or quietly resolveable problems should not use an exception to indicate that the problem has occurred.

5
  • Good answer. I think when you have just a single point of failure it's easy to avoid exceptions by returning false or null. But in case when method is a bit more complex and it can have many different points of failure. Some are less common some are more common I think it's better to stick with one way of handling errors.
    – Konrad
    Sep 22 '21 at 13:43
  • Unfortunately expressing errors in C# is very poor as there is no standard way and most libraries throw exceptions even for errors that can be common.
    – Konrad
    Sep 22 '21 at 13:45
  • @Konrad: "it's easy to avoid exceptions by returning false or null" Wantonly returning null is (I cannot bold this enough) THE main source of unexpected exceptions. You're not avoiding exceptions, you're passing the buck to have exceptions crop up somewhere else. That's not a solution.
    – Flater
    Sep 22 '21 at 15:29
  • True, that's why compile-time checked Option<T> is so powerful in Rust. Anyway if you use Rider then it will highlight if you use something that can be null. Still I think it's better performance-wise than throwing NotFoundException
    – Konrad
    Sep 22 '21 at 16:06
  • @Konrad The issue isn't with the performance of returning null, it's the subsequently likely null reference exception, combined with the need for a codebase littered with null checks, and the difficulty of debugging null reference exceptions which are a whole lot less informative than the explicit exception you would've knowingly thrown instead. A result DTO is trivial to implement, even if not provided as a C# standard implementation, and avoids all of the above problems.
    – Flater
    Sep 22 '21 at 20:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.