We, as C# developers, are used to throwing exceptions everywhere be it a functional such as validation or technical such as network failure. But is it worth throwing exceptions everywhere? Exceptions are expensive. You should not throw exceptions everywhere. You should throw exceptions only when it is necessary. But how? Let’s discuss the alternative approach.
Types of exceptions in general
Excellent article by Eric Lippert on exceptions, where you can find the types of exceptions in general. I will discuss the four categories of exceptions in short and will discuss in which category you should throw exceptions and in which category you should not throw exceptions and also discuss the alternative approach.
As per Eric Lippert, there are four categories of exceptions in general.
- Fatal
- Vexing
- Exogenous
- Boneheaded
Fatal
Fatal exceptions are the ones that you can’t do anything about and not your fault. For example, OutOfMemoryException
. You can’t do anything about it. You can’t recover from it. You can’t retry it. You can’t do anything about it. So, there is no point in catching it. You should let it bubble up to the top.
Vexing
Vexing exceptions are the ones that you can avoid by changing the code. For example, FormatException
. You can avoid it by using TryParse
instead of Parse
. So, you should not throw vexing exceptions. You should use TryParse
instead of Parse
or similar approach.
Exogenous
Exogenous exceptions are the ones that you can’t avoid by changing the code. For example, FileNotFoundException
. You can’t avoid it by changing the code. For example, if you are trying to read a file and the file does not exist. You may try fail fast approach by checking the file existence before reading it.
But what if the file is deleted/renamed/moved/locked after checking the existence and before reading it? to avoid this, you can lock the file maybe? But what if network failure happens.
As we can see, how hard we try to avoid it, we can’t avoid it. So, in such cases you should throw exogenous exceptions.
Boneheaded
Boneheaded exceptions are the ones that you can avoid by changing the code. For example, ValidationException
or IndexOutOfRange
. You can avoid it by checking the argument. So, you should not throw boneheaded exceptions. You should validate the argument and in latter case, you should check the index before accessing the array.
Alternative approach : Dropping exceptions,treat as rule
Let’s say you are creating user with an email. There could be two business scenarios apart from infrastructure issues.
- Either user with the same email already exists.
- We should be able to create user with the email.
In general, what we should is create a DuplicateEmailException
and throws it from the service. But as per the above discussion, we should not throw it.
Instead what we could do is return a ProblemOr, in this case if the user with the same email already exists, we will return a Problem with the status code Conflict and if the user with the same email does not exist, we will return a User.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public async Task<ProblemOr<User>> CreateUserAsync(string email)
{
if (await _userRepository.GetAsync(email) is User user)
{
return Problem.Conflict("Email already exists");
}
var probableUser = User.CreateWithEmail(email);// This might have email validation logic as well.
if (user.HasProblem)
{
return probableUser.Problems;
}
await _userRepository.AddAsync(probableUser.Value);
return probableUser.Value;
}
What we did here basically instead of throwing an exception, we are treating as control flow. We are not breaking the chain but still we are bubbling up the problem to the top. So, the caller can decide what to do with the problem like below:
1
2
3
4
[HttpPost]
probableUser.SwitchFirst(
user => Console.WriteLine(user.Name),
error => Console.WriteLine(error.Description));
It is also possible that the method does not return any business object. For example, you are sending email to a user and you just want to return Success or Failure. In this case, if the user does not exist, you can return Problem with the problem as NotFound and if the user exists, you can send email and return Success, Like below:
1 |
|
We can now see the alternative approach in action. But what is this ProblemOr? Let’s discuss it.
ProblemOr : An opinionated discriminated union in C#
ProblemOr is nothing but an opinionated discriminated union. Discriminated union provides a way to represent a value that can be one of a fixed set of different values and types. Discriminated unions are useful for heterogeneous data; data that can have special cases, including valid and error cases.
ProblemOr can be one of the following:
- Problem : Represents a problem with the status code and description.
- Result : Represents a success.
- T : Represents a value of type T.
I mentioned it as opinionated because this is not a general purpose discriminated union, where you can have any number of cases. This is specifically designed for the scenarios where you want to return a value or a problem.
You need to install the package from here to use it.
Conclusion
In this article, we discussed the types of exceptions in general and in which category you should throw exceptions and in which category you should not throw exceptions and also discussed the alternative approach. We also discussed the ProblemOr which is an opinionated discriminated union in C#.