The answer is in the Result, just don't throw;

Exceptions are expensive and hard to read. Discover how using the Result monad from functional programming can improve your app's performance and code readability

The answer is in the Result, just don't throw;
Photo by Zach Lucero / Unsplash

Using exceptions can hurt performance, especially in web apps, as they can be expensive to throw and catch. When an exception is thrown, the runtime creates an exception object containing information about the exception, such as the type, message, and stack trace. This object is then passed up the call stack until a catch block is found that can handle the exception. This process can be costly, especially if the exception is thrown frequently or the call stack is deep.

In addition to the performance overhead, exceptions can make your code more difficult to read and understand. Exceptions are designed to be used for exceptional cases, such as when a system is out of memory, or a file cannot be found. However, in web applications, it's common to use exceptions for handling errors that are part of the normal flow of the application, such as invalid input. This can make it hard to understand the expected behavior of the code and make it difficult to write automated tests for the code.

A Result monad is a more efficient alternative for handling errors. Instead of throwing exceptions, a Result monad returns a result object containing information about an operation's outcome. This allows you to handle errors more explicitly and predictably without the performance overhead of exceptions.

Here are some key advantages of using the Result monad pattern over exceptions:

  • Explicit error handling: By using the result monad pattern, you explicitly indicate that a method can return an error rather than relying on exceptions to indicate errors. This makes your code more readable and understandable and makes it easier to reason about the flow of your application.
  • Predictable error handling: With the Result monad pattern, you can handle errors consistently and predictably throughout your application. This makes it easier to write testable and maintainable code.
  • Explicit error types: By using explicit error types, you can take advantage of C#'s type system to catch and handle specific types of errors. This makes your code more readable and maintainable.
  • Performance impact: Exceptions are more expensive than returning a result. Exceptions are slow and allocate memory on the heap. This can have a significant impact on the performance of your application, especially in high-throughput scenarios. With the result monad pattern, you can avoid the performance overhead of exceptions.
  • Separation of concerns: By using the Result monad pattern, you can separate the error-handling logic from the controller and make it more reusable. This makes it easier to test and understand the logic of the service.
  • Easy to implement: The Result monad pattern is simple and easily integrated into existing code bases. This makes it easy to start using it in your projects right away.
  • Flexibility: You can use different libraries that implements the Result Monad, which allows you to have a more robust and flexible error-handling mechanism.

While the Result monad pattern has many advantages, there are also cases where it may not be the best choice. Here are some situations where it may not be appropriate to use the Result monad pattern:

  • Simple Applications: If your application is relatively simple and has few error scenarios, then the Result monad pattern may be overkill. In these cases, it may be simpler to use exceptions for error handling.
  • Legacy Code: If you are working with legacy code that heavily relies on exceptions, it may not be easy to retrofit it to use the Result monad pattern. In these cases, it may be better to stick with the existing exception-based error handling.
  • Debugging: Exceptions provide more information when it occurs, it is easier to trace and debug, it gives you the stack trace and other information that may not be available when using the Result monad pattern, and it may be more difficult to trace an error in the application when using the Result monad pattern.
  • Interoperability: The Result monad pattern may be less compatible with other libraries and frameworks that rely on exceptions for error handling.

Overall, the Result monad pattern is a powerful tool for error handling, but it's not always the best choice. It's important to weigh the advantages and disadvantages of the pattern and choose the approach that best fits your use case.

Here is an example of a simple web API controller that uses a Result monad to handle errors:

All the code for this article can be found over at Github: https://github.com/suddenelfilio/answer-is-in-the-result

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class CalculatorController : ControllerBase
{
    private readonly ICalculatorService _calculatorService;
    
    public CalculatorController(ICalculatorService calculatorService)
    {
        _calculatorService = calculatorService;
    }
    
    [HttpGet("divide/{a}/{b}")]
    public async Task<IActionResult> Divide(double a, double b)
    {
        var result = await _calculatorService.Divide(a, b);

        if (result.IsError)
        {
            return BadRequest(result.Error);
        }

        return Ok(result.Value);
    }
}

In this example, the Divide action of the CalculatorController takes two double values and returns the result of dividing a by b. The Divide method of the CalculatorService is responsible for performing the division operation and returning a result object. If the operation is successful, the result instance contains the result of the division. If the operation fails, the result instance has a DivideByZeroError object.

The Divide action checks the result object to determine whether the operation was successful without the need for throwing or catching exceptions. This can improve performance, as exceptions can be expensive to throw and catch. Additionally, using the FluentResults library and explicit error types, you can make your code more readable and maintainable.

using FluentResults;

namespace answer_is_in_the_result;

public class CalculatorService: ICalculatorService
{
    public Result<double> Divide(double a, double b)
    {
        return b == 0 ? Result.Fail<double>(new DivideByZeroError()) : Result.Ok(a / b);
    }
}

As you can see, the Divide method of the CalculatorService class checks if the denominator is zero; if it is, it returns a failing result with an DivideByZeroError object. If the denominator is not zero, it returns a successful result with the division of the numerator and denominator.

By implementing the service this way, we have separated the error-handling logic from the controller and made it more reusable. This makes it easier to test and understand the logic of the service. Additionally, by using the Result class from the FluentResults library, we have made the code more readable and explicit.

It's worth noting that this is a simple example, but in real-world applications, you will typically have more complex logic and multiple cases where errors can occur. Using the result monad pattern, you can handle these errors consistently and predictably throughout your application.

Here are some examples of tests for the CalculatorService.Dividemethod:

using answer_is_in_the_result;
using FluentAssertions;

namespace Tests;

public class Tests
{
    [Fact]
    public void Divide_WhenDivideByZero_ShouldReturnError()
    {
        var calculatorService = new CalculatorService();
        var result = calculatorService.Divide(1, 0);
        result.IsFailed.Should().BeTrue();
        result.Errors.Should().HaveCount(1);
        result.Errors.First().Should().BeOfType<DivideByZeroError>();
    }

    [Theory]
    [InlineData(1, 2, 0.5)]
    [InlineData(3, 4, 0.75)]
    public void Divide_WhenValidInput_ShouldReturnResult(double a, double b, double expectedResult)
    {
        var calculatorService = new CalculatorService();
        var result = calculatorService.Divide(a, b);
        result.IsFailed.Should().BeFalse();
        result.Value.Should().Be(expectedResult);
    }
}

In these examples, we are using xUnit for testing and FluentAssertion for asserting results. The first test case checks the case when dividing by zero and should return a DivideByZeroError, as expected.

The second test case is checking for valid input; it should return a Result<double> object containing the expected result of the division.

using FluentResults;

namespace answer_is_in_the_result;

public class DivideByZeroError : Error
{
    public DivideByZeroError() : base("Cannot divide by zero.") { 
        WithMetadata("ErrorCode","1");
    }
}

This class inherits from the Error class, and it can be used to create explicit error types. It's simple, and it has a constructor that calls the base class constructor and passes a string message: "Cannot divide by zero."

You can create as many custom error types as you need, depending on your application's different types of errors. This allows you to handle errors more explicitly and predictably without relying on strings to represent error messages.

By using explicit error types, you can also take advantage of C#'s type system to catch and handle specific errors, making your code more readable and maintainable.

In these examples, I used the FluentResults library, but there are many flavors of this implementation to be found on Nuget - search them at https://www.nuget.org/packages?packagetype=&sortby=relevance&q=Result&prerel=True

In conclusion, using exceptions in web APIs can lead to poor performance and a lack of control over error handling. The Result monad pattern provides an alternative approach by explicitly returning error information. As a result object. This allows for better error handling control and leads to more efficient and predictable code. However, it's essential to consider the specific needs of your application and weigh the advantages and disadvantages of using the result monad pattern. In some cases, exceptions may be the more appropriate choice.