The Problem
Exceptions are slow, cumbersome, and often result in unexpected behavior. Even the official Microsoft docs tell you to limit your use of exceptions.
Most of the time, you want to handle both the success and failure cases without allowing the exception to propagate. You might be wondering "if I won't use exceptions, how can I tell the caller function that something went wrong?". This is where the result type comes in.
The Result type
When our function needs to represent two states: a happy path and a failure path, we can model it with a generic type Result <T, E>
where T represents the value and E represents the error. A function that gets a user could look like this:
public async Result<User, string> FindByEmail(string email) {
User user = await context.Users.FirstOrDefaultAsync(
u => EF.Functions.Like(u.Email, $"%{email}%"));
if(user is null) {
return "No user found";
}
return user;
}
You would call the function like this:
[HttpGet("{email}")]
public async Task<ActionResult<User>> GetByEmail(string email)
{
if(string.IsNullOrEmpty(email)) {
return BadRequest("email cannot be empty");
}
Result<User, string> result = await FindByEmail(email);
return result.Match<ActionResult<User>>(
user => Ok(user),
_ => NotFound());
}
If you don't want to return strings for errors, but instead a different type, you can define those classes/structs and return them or use the existing Exception types for your errors. Returning exceptions is fine, throwing them is what's costly.
Here is the code for the result type:
public readonly struct Result<T, E> {
private readonly bool _success;
public readonly T Value;
public readonly E Error;
private Result(T v, E e, bool success)
{
Value = v;
Error = e;
_success = success;
}
public bool IsOk => _success;
public static Result<T, E> Ok(T v)
{
return new(v, default(E), true);
}
public static Result<T, E> Err(E e)
{
return new(default(T), e, false);
}
public static implicit operator Result<T, E>(T v) => new(v, default(E), true);
public static implicit operator Result<T, E>(E e) => new(default(T), e, false);
public R Match<R>(
Func<T, R> success,
Func<E, R> failure) =>
_success ? success(Value) : failure(Error);
}
The implicit operators allow you to return a value or error directly. For example, return "err"
can be used instead of return new Result<User, string>.Err("err")
. It will automatically convert it to a result type for you.
Performance comparison
I wrote a benchmark that compares returning a result with throwing an exception. The benchmark compares the identical functions if they fail 0% of the time, 30% of the time, 50% of time, and 100% of the time.
Here are the results:
What we care about here is the mean. You can see that returning the result directly and matching outperforms exception handling as exceptions get thrown more often.
In reality, your function will probably not throw exceptions 50% of the time but this is just a benchmark to illustrate how slow exceptions can be if used often.
This is the benchmark code:
[Params(0, 30, 50, 100)]
public int failPercent;
[Benchmark]
public int[] result() {
int[] res = new int[100];
for(int i = 0; i < 100; i++) {
res[i] = getNum_Res(i < failPercent).Match<int>(
n => n + 10,
err => 0);
}
return res;
}
[Benchmark]
public int[] exception() {
int[] res = new int[100];
for(int i = 0; i < 100; i++) {
try {
res[i] = getNum_Exception(i < failPercent) + 10;
}catch(Exception) {
res[i] = 0;
}
}
return res;
}
public Result<int, string> getNum_Res(bool fail) {
if(fail) {
return "fail";
}
return 1;
}
public int getNum_Exception(bool fail) {
if(fail) {
throw new Exception("fail");
}
return 1;
}
Closing Thoughts
Throwing less exceptions means writing less lines of code, and handling errors more efficiently. Remember, "The best code, is no code at all". Have a good day :)
Top comments (5)
Everytime I see the suggestion of using Resultobject instead Exception. I postulate that they don't know the difference between the normal flow of a program and the abnormal (error, exception, ...) flow.
Here in your exemple it's perfectly normal that when searching for something, you don't have a hit. So ofc you should never throw a NotFoundException here. But having a ResultObject here is wrong because when you search something and don't find it, you just return null. But if, while searching your object, you connection to the database vanish, then it makes perfectly sense to throw an exception.
Then you can argue when should I use that kind of pattern ? This pattern makes a lot of sense in command/event programing where, for a command, you have 2 kind results: CommandSuccceed and CommandFailed. Here you keep the exception for "transport" problem and put the failure object for business information. You can see that kind of usage in Microsoft.CognitiveServices for example
I understand the fundamental difference between normal flow and the abnormal.
This example is admittedly a bit unrealistic: If the object is not found, you can just return null.
Like I mentioned in the article, you can return an Exception object in the result instead of throwing it if you want custom Exception types. After using rust, I found that using a Result with a match function forces you to handle both success and failure and leaves you many options in handling that error. Result is malleable and you can sculpt it to fit your system's needs.
You seem like a seasoned developer based on your profile, I'm sure you've come across codebases with tons of try-catch blocks for normal flows that could be better handled with something as simple as the solution I offered. And even for abnormal flows, you can have result types with custom types to differentiate those if you willed. Or if it's fatal, just let the exception be thrown and catch it in the middleware in those special cases.
I personally never fiddled with CQRS but what you said about it being ideal with commands and events makes sense.
I would argue the opposite. The standard error handling in .NET is using exceptions. Returning types as errors is so wrong in so many ways. The biggest issue is, it swallows actual error messages from telemetry.
You can argue that you can try catch the error, log it and then return the result type. What's the point of having result types then?
I never argued to try catch an error then return a result type. Languages with no exception throwing like go and rust handle this by simply using custom error types. You could use different classes to represent different error types in your result or just return an exception in your result instead of throwing it like I mentioned in the article. This is not a silver bullet but I personally find it much more logical than try catch finally blocks.
If a fatal exception happens, just let it propogate and be caught by some middleware but that should only ever happen for errors you can't really handle such as the DB connection being closed
What I prefer is simply let the caller take care of data integrity. If methods dont receive valid data, just throw an exception. That way you dont lose stack trace. Caller should handle the exceptions it wants and let other exceptions go through.
Finally, you can have a middleware to catch the remaining errors.