With more languages (C# for example) offering an “Exception” model for handling errors, A new question has been raised regarding the development of our applications, should we return an Error Object or throw an Exception (throw vs return)?, and the typical answer that you will find is “It depends”, so in this article, we will try to deconstruct this answer to find out why and when to use each method.
if you looked around and searched on error handling, all results will come up around exceptions. even if the idea of Result objects is not new, developers tend to go with Exceptions in most cases, and it ok to choose Exceptions there is nothing wrong with them, in fact, there are some cases where Exceptions are the best solution. and with the features offered by programming languages, It is really easy to work with them and seamlessly integrate them into your code, you just need to know when it is best to use them and when it is not.
Exceptions, What.?
so let’s first define what is an Exception, in programming, Exceptions are defined as unexpected or exceptional situations that occur during the execution of a running program, ranging from sample errors to hardware-related issues.
say for example we have a function that sends a notification email to a user and takes as arguments the user email and templateId
public void SendNotificationEmail(string userEmail, string templateId)
{
if (string.IsNullOrEmpty(userEmail))
throw new ArgumentException("user email is null or empty", nameof(userEmail));
if (string.IsNullOrEmpty(templateId))
throw new ArgumentException("templateId is null or empty", nameof(templateId));
var template = LoadTemplate(templateId);
if (template is null)
throw new EmailTemplateNotFoundException(templateId);
sendEmail(new SendTemplateEmailOptions {
To = userEmail,
template = template,
From = _appSettings.DefaultFrom(),
SmtpOptions = _appSettings.GetSmtpOptions(),
});
}
for this function to work properly and as expected all arguments should be valid, so if:
- any given argument is null we have an exception.
- if the email is not valid or the format is not as excepted, we have an exception.
- if the template with the given ID doesn’t exist we have an exception.
as you can see we have secured the execution of our method by adding validation to those exceptional scenarios, if any validation failed we stop the execution by throwing the appropriate exception type, and It will be the job of the caller of our method to deal with the exceptions being thrown.
What about “Result Object”?
Think of it as a lightweight object that describes the result of an operation, with details about what went wrong. So that instead of throwing an exception, we return an object.
let’s take the previous example and refactor it to use the Result Object.
public Result SendNotificationEmail(string userEmail, string templateId)
{
if (string.IsNullOrEmpty(userEmail))
return Result.Failure()
.WithMessage("user email is null or empty")
.WithCode("user_email_required");
if (string.IsNullOrEmpty(templateId))
return Result.Failure()
.WithMessage("templateId is null or empty")
.WithCode("template_id_required");
var template = LoadTemplate(templateId);
if (template is null)
return Result.Failure()
.WithMessage("couldn't locate any email template with the given id")
.WithCode("email_template_not_found")
.WithMataData("templateId", templateId);
sendEmail(new SendTemplateEmailOptions {
To = userEmail,
template = template,
From = _appSettings.DefaultFrom(),
SmtpOptions = _appSettings.GetSmtpOptions(),
});
return Result.Success();
}
so as you can see instead of throwing exceptions we are returning a Result Object that defines the execution status of the function.
here we are using the Result.Net library for a pre-defined result object.
And now after we defined the difference, let’s dive deep.
What are the benefits of using Exceptions?
as you have seen exceptions are a native approach for dealing with exceptional scenarios. and they are supported by almost all programming languages. and there is no hard way to work with them, you just throw and catch.
they are also very descriptive and can provide a full context of the error, like an error message, and a call stack also metadata associated with the error/exception. which makes them very helpful when debugging.
Note
you can check this article (working with exceptions in Dotnet c#) for a more detailed explanation on how to work with exceptions in the C# language.
we can resume the pros of using exceptions as:
- Native approach for handling errors.
- Clean and descriptive.
- Easy to use
- Native debugging supported.
What about Result Object?
Result objects intend to encapsulate the execution of a code by indicating success or failure so that as an alternative of using try/catch when using the exception, we simply use the If statement.
have a look at these two code samples:
1- handling execution of SendNotificationEmail function that uses exceptions.
try {
SendNotificationEmail("[email protected]", "order_processed");
}
catch (EmailTemplateNotFoundException ex) {
// execution has failed because the email template could not be found.
}
catch (Exception ex) {
// the execution of the method has failed.
}
2- handling execution of SendNotificationEmail function that uses Result Object.
var result = SendNotificationEmail("[email protected]", "order_processed");
if (result.IsFailure()) {
// the execution of the method has failed.
if (result.FailedBecause("email_template_not_found")) {
// execution has failed because the email template could not be found.
}
}
it obvious that the second method is much cleaner and direct and gives you a smooth execution path.
with try/catch is somehow similar to the goto statement because you leave the scope of where you called the method and go to the catch statement scope, this can be overwhelming in cases where you have complicated logic.
and same as the Exception, the Result object can carry metadata to describe the execution of the code. like a message, code, etc. it’s based on what you have defined in your result object, or if you’re using a library like Result.Net where the object already has a pre-defined schema, you can have a full context on what happened during the execution of the code.
as pros of using Result Objects we have:
- Very easy and simple to use.
- Mush cleaner to handle.
- Way more extendable & customizable.
What about Performance?
I created a simple console project (you can find it on Github), with 4 functions two of them are using Exceptions and the other two are using Result Object, each function will process the same amount of items and run the same logic.
- UsingExceptions_WithoutErrors: this function is using exceptions and doesn’t throw any exceptions.
- UsingExceptions_WithErrors: this function is also using exceptions but it does throw one.
- UsingResultObject_WithoutErrors: this function is using the Result Objects and doesn’t return any error.
- UsingResultObject_WithErrors: same as the previous this one uses Result Objects but it returns an error.
I used BenchmarkDotNet to measure the execution performance, so by running the command:
dotnet run -p ToThrowOrToReturn.csproj -c Release
the results of processing 1000 items:
![the benchmark result for the test 1](http://youssefsellami.com/wp-content/uploads/2021/07/benchmark-result.png)
the results of processing 10000 items:
![the benchmark result for the test 2](http://youssefsellami.com/wp-content/uploads/2021/08/benchmark-result-2-1024x479.png)
obviously, you can tell the distinction, there is a huge difference between the two, let analyze what we have.
1- without errors:
in this case, the function that use’s the result object is a little bit slower than the one that uses exceptions. and the reason is that we pay the cost of creating the success result object because the function must return a result instance. On the other hand, the one that uses exceptions doesn’t throw any exceptions it only executes the core logic and nothing else.
2- with errors:
for this case, we see a HUGE gap between the two, the function that uses exceptions is 18 times slower than the one that uses the result object, the reason we see this increase is because we are paying a double cost, one for the creation of the exception object and the second one for the catch block.
even that the numbers are really small 11,322.51 us ≈ 0.01 s and are barely noticeable we should keep in mind that we are always looking to optimize our code, this is why this type of comparison gives us a clear version of what is best for us.
now the final question
Throw vs Return: What to use, and when to use?
you may say that it is better to use Result Objects instead of exceptions because exceptions are slower, and you are right, do prefer result objects over exceptions, but there are cases where we must use exceptions.
when you have a function that must be executed without any errors, use exceptions to stop the execution if you encounter any invalid or exceptional step.
say for example we have a function called LoadFileData(string filePath) the function must read the content of the file and do some work, we know that this function must not fail so in order to secure it we should define what are the potential failure cases. for example, filePath is null, filePath is empty, filePath is an invalid path, the file does not exist, and any other thing that may cause the failure. so we must throw an exception for any of these cases so it will be the responsibility of the consumer of our function to do the necessary validation before calling our function. do recall in performance test we find that functions that use exception without throwing are much faster.
when you define a logic where some of its arguments are required and must be supplied correctly use exceptions to communicate the necessity of having valid arguments.
only use Result Objects when you want to communicate the execution status of a logic where you are not certain of what the final result is.
avoid using result objects and exceptions, always make your code secure and check for potential failure causes, if you encounter something unexpected use exceptions to communicate the error, exceptions are a great way of saying this must be fixed.
so to resume:
– exceptions must be used to only secure the execution of our logic. they should not be thrown and if they do throw, the cause must be fixed.
– use result objects to communicate the execution status of a logic where you’re no certain of what may be the final result.
try to avoid using exception and result objects, always check for error conditions and act accordingly.
Great article and definitely not easy topic, thank you for summing this up 🙂
In your example with `LoadFileData` it’s questionable whether exception should be thrown if file does not exist. I would say it depends. If I’m blindly reading files from drive then non-existing file seems to be somewhat expectable.
I wrote simple benchmakrs using custom readonly struct result type (having only `Value` and `Error` property) and I see different numbers. Could you check what overhead does `Result.Net` add? Has anything changed in .NET 6? For me, both success and failure with Result type are 5x times faster than success without exception, and both success and failure using result type are identical.
Last but not least allow me to add additional comments to pros of using exceptions:
– Clean and descriptive: but only if you document it! Otherwise caller may have no clue about your method throwing. (I wish it was part of signature like in Java).
– Easy to use: easy to throw, headache to handle exceptions properly in the right place ¯\_(ツ)_/¯
Thanks for your comment,
regarding the “LoadFileData” function, in the example that I give, we expect the function to execute without any failures, and yes as you said, “If I’m blindly reading files from drive then non-existing file seems to be somewhat expectable.” you’re right but in our example, we have limited the function to only read the file content. so if the file for example doesn’t exist it should be controlled by the one who is calling this function.
I re-run the same benchmarks on .NET 6.0.2, for the 1000 items iteration and the results are deferents, there is a gain of X2 in performance.