Avoid Exceptions
"Exception handling is the process of responding to the occurrence of exceptions – anomalous or exceptional conditions requiring special processing – during the execution of a program." - Wikipedia
Exceptions vs Errors
Exceptions and errors are not the same thing:
- Exceptions should be only used for exceptional situations, which by definition can not be many
- Fewer exceptions - the better
- Exceptions should not leave the sysem boundaries in their original form
- It is not user friendly and gives attackers a way to further explore possible system weaknesses
- If the thrown exception is processed by our application, we should not use an exception
- It is slow and we can deal with it within our boundaries
- Use another mecanism like
Result
, a Monad likeEither
, a callback, ...
- Error/Result explicitly declares the possibility of an error and allows “linear” processing
- An implementation on exceptions will be a hidden goto statement
- The worse the processing code is from the exception code of an exception, the worse it will be
Why?
We should avoid avoid the intensive usage of Exceptions
for:
- Making our code the more transparent possible, and so the more readable possible
- Avoid lies in our method / function definitions
- Keeping them for exceptional situations
Problems
- How can I create more explicit methods or functions?
- How can I deal with errors in code?
How to
- We have different options to avoid exceptions
- Let's take this example and present a few of them:
- Imagine we have a simple
Divide
method in our code - This method contains a lie in terms of contract / public API
- It says that for 2 given
double
the method returns adouble
:double -> double -> double
- What happens if we pass 0 as a denominator?
- The defintion contains a
lie
(maybe by omission)
- It says that for 2 given
- Imagine we have a simple
public static double Divide(double numerator, double denominator)
=> numerator / denominator;
Extend the output
We can do it by using a Result
type:
- Here is a simple one created for this example
public static Result<double, string> Divide(double numerator, double denominator)
=> denominator == 0
? Failure("Invalid denominator")
: Success(numerator / denominator);
- Its basic implementation
public record Result<TSuccess, TFailure>
where TFailure : class
{
private readonly TSuccess? _success;
private readonly TFailure? _failure;
private Result(TSuccess success) => _success = success;
private Result(TFailure failure) => _failure = failure;
public static Result<TSuccess, TFailure> Success(TSuccess success) => new(success);
public static Result<TSuccess, TFailure> Failure(TFailure failure) => new(failure);
public void Match(Action<TSuccess> onSuccess, Action<TFailure> onFailure)
{
if (IsFailure())
onFailure(_failure!);
else onSuccess(_success!);
}
private bool IsFailure() => _failure is { };
public TSuccess IfFailure(TSuccess ifFail)
=> IsFailure()
? ifFail
: _success!;
}
- As a consumer / caller of this method we now have to deal with the return type:
- We have to be exhaustive in the treatment of the result
- Our method does not contain lie anymore and express explicitly that the operation can fail:
double -> double -> Result
result.Match(success => Console.WriteLine($"Success {success}"),
failure => Console.WriteLine($"Failure: {failure}"));
Alternatively you may use monads like
Either
,Try
,Option
,Maybe
, ...
Constrain the input
It has huge advantages of constraining inputs / arguments:
- You don't need to write preventive code anymore (No more guard clause everywhere)
- We make it impossible to represent invalid state
- We have a more business related concepts that are expressed in our code
public static double Divide(double numerator, NonZeroDouble denominator)
=> numerator / denominator.ToDouble();
- Here we express the fact that for instantiating a
NonZeroDouble
we need to pass a validdouble
different from 0
public record NonZeroDouble
{
private readonly double _value;
private NonZeroDouble(double value) => _value = value;
public static NonZeroDouble From(double value)
=> value == 0
? throw new ArgumentException("0 is not allowed for NonZeroDouble")
: new NonZeroDouble(value);
public double ToDouble() => _value;
}
public static class DoubleExtensions
{
public static NonZeroDouble ToNonZeroDouble(this double value) => NonZeroDouble.From(value);
}
-
We centralize its instantiation logic of this kind of data structure through
- A
Factory Method
- A
private constructor
- An
extension method
to convert fromdouble
- A
-
We could return a default value instead of throwing an
Exception
as well- It is a business decision at the end
-
As a consumer / caller of this method we now have to deal with the new input type
- Our method expresses explicitly what is accepted as input:
double -> NonZeroInteger -> double
- Our method expresses explicitly what is accepted as input:
var result = Divide(9, 3d.ToNonZeroDouble());
Hollywood principle
Don't Call Us, We'll Call You
-
One of the most popular way to implement this principle is to use
events
orcallbacks
-
The basic idea behind it is
let the caller decide what happens next
-
Let's use callbacks / continuation functions in our
Divide
method
public static void Divide(
double numerator,
double denominator,
Action<double> onSuccess,
Action<string> onError)
{
if (denominator == 0) onError("Invalid denominator");
else onSuccess(numerator / denominator);
}
- As a consumer / caller of this method we now have to pass callback methods for success and failure
- The method returns
void
now:double -> double -> (double -> void) -> (string -> void)
-> void
- The method returns
Divide(9,
0,
success => Console.WriteLine($"Success {success}"),
failure => Console.WriteLine($"Failure : {failure}"));
- As a side effect, continuation has complexified our method signature
- Use this principle with care to avoid callback hell
Other alternatives exist and can depend on your programming language / paradigm
Constraint
- If you are already using
Exceptions
to handleerrors
remove them using one of the alternative presented or another - If not, think about a first edge that you do not support yet and use one of the alternative presented to manage it