Stopping Using Exception Use Result Monad Instead

Cyril Canovas
Cyril Canovas
Stopping Using Exception Use Result Monad Instead
Table of Contents
Table of Contents

Introduction

Error handling is an inevitable part of software development. Traditionally, exceptions are the primary method for signaling that something has gone awry. However, the exception mechanism is fraught with issues: unpredictable control flow, performance bottlenecks, and challenges in debugging. Imagine a world where error handling is graceful, predictable, and efficient. Enter the Result Monad—a powerful alternative to exceptions that can streamline error handling, improve code maintainability, and make your programs more robust.

The Problem with Exceptions

Exceptions might seem like a convenient error-handling mechanism, but they introduce several issues. For starters, they disrupt the program's control flow, causing jumps that are difficult to manage and predict. Debugging such issues can quickly become a nightmare. Further, the overhead involved in generating and throwing exceptions can be significant, especially in performance-critical applications. Lastly, their implicit behavior makes the code harder to reason about. You might find yourself asking, "Where can this exception come from, and what can it contain?"

Understanding Result Monad

The Result Monad originated from functional programming and provides a structured way to handle computations that might fail. At its core, the Result Monad encapsulates a value that can either be a success or a failure, without throwing exceptions. This makes error handling explicit and predictable, enhancing code readability and maintainability. Typical use cases include API responses, file I/O operations, and database transactions. Essentially, any operation that can either succeed or fail can benefit from using a Result Monad.

Implementing Result Monad

To demonstrate the implementation, let’s use C# and the languageext.core NuGet package:

Install the Package:

dotnet add package LanguageExt.Core

Business Services Implementation:

With Exceptions:

public class BusinessServiceWithException : IBusinessServiceWithException
{
    public async Task<string> Get(string id)
    {
        await Task.CompletedTask;
        
        if (string.Compare("HasValue", id, StringComparison.CurrentCultureIgnoreCase) == 0)
            return id + " success";
        
        var notFoundException = new NotFoundException(id);
        throw notFoundException;
    }
}

With Result Monad:

public class BusinessServiceWithResult : IBusinessServiceWithResult
{
    public async Task<Result<string>> Get(string id)
    {
        await Task.CompletedTask;
        
        if (string.Compare("HasValue", id, StringComparison.CurrentCultureIgnoreCase) == 0)
            return id + " success";
            
        var notFoundException = new NotFoundException(id);
        return new Result<string>(notFoundException);
    }
}

Controllers Implementation:

With Exceptions:

[ApiController]
[Route("[controller]")]
public class TestWithExceptionController(
    ILogger<TestWithExceptionController> logger,
    IBusinessServiceWithException businessServiceWithException)
    : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<ActionResult<string>> Get(string id)
    {
        var result = await businessServiceWithException.Get(id);
        return Ok(result);
    }
}

The exceptions are tranformed to the correct server response by a middleware:

public class ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
   private readonly RequestDelegate _next = next ?? throw new ArgumentNullException(nameof(next));
   private readonly ILogger<ExceptionHandlingMiddleware> _logger = logger ?? throw new ArgumentNullException(nameof(logger));

   public async Task InvokeAsync(HttpContext context)
   {
      try
      {
         await _next(context);
      }
      catch(NotFoundException ex)
      {
         //_logger.LogError(ex, "Entity not found.");
         await HandleNotFoundExceptionAsync(context, ex);
      }
      catch (Exception ex)
      {
         _logger.LogError(ex, "An unhandled exception has occurred.");
         await HandleExceptionAsync(context, ex);
      }
   }

   private static Task HandleNotFoundExceptionAsync(HttpContext context, NotFoundException notFoundException)
   {
      context.Response.ContentType = "application/json";
      context.Response.StatusCode = (int)HttpStatusCode.NotFound;
      return context.Response.WriteAsJsonAsync(new { message = notFoundException.Message });
   }

   private static Task HandleExceptionAsync(HttpContext context, Exception exception)
   {
      context.Response.ContentType = "application/json";
      context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
      // You can customize the response as needed. 
      // Here, we're sending a simple JSON response.
      var response = new { message = "Internal Server Error" };
      
      return context.Response.WriteAsJsonAsync(response);
   }
}

With Result Monad:

[ApiController]
[Route("[controller]")]
public class TestWithResultController(
    ILogger<TestWithResultController> logger,
    IBusinessServiceWithResult businessServiceWithResult)
    : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<ActionResult<string>> Get(string id)
    {
        var result = await businessServiceWithResult.Get(id);
        return result.Match<ActionResult<string>>(success => Ok(success),
            failure =>
            {
                return failure switch
                {
                    NotFoundException notFoundException => StatusCode((int)HttpStatusCode.NotFound,
                        new { message = notFoundException.Message }),
                    _ => StatusCode((int)HttpStatusCode.InternalServerError,
                        new { message = "Internal Server Error" })
                };
            }
        );
    }
}

Benchmark results

Let's further solidify this with some benchmarking to quantify the performance differences between the two approaches:

public class Benchmarks
{
    private readonly HttpClient _httpClient = new HttpClient();
    private const string TestWithResultUrl = "http://localhost:56303/TestWithResult/NoMatchingValue";
    private const string TestWithExceptionUrl = "http://localhost:56303/TestWithException/NoMatchingValue";

    [GlobalSetup]
    public void Setup() {}

    [Benchmark]
    public async Task TestWithResultController()
    {
        var response = await _httpClient.GetAsync(TestWithResultUrl);
        await response.Content.ReadAsStringAsync();
    }

    [Benchmark]
    public async Task TestWithExceptionController()
    {
        var response = await _httpClient.GetAsync(TestWithExceptionUrl);
        await response.Content.ReadAsStringAsync();
    }
}

First start the WepApi:
start_web_api.png
Here are the benchmark results:
benchmark_results.png

In this case TestWithExceptionController.Get is x 1.6 slower than TestWithResultController.Get.

Comparative Analysis: Exceptions vs. Result Monad

Let's break down the differences:

  • Readability: The Result Monad makes error handling explicit, making code more readable. Exceptions, on the other hand, can create hidden paths in the code flow that are harder to follow.
  • Performance: Avoiding exceptions eliminates the overhead associated with generating stack traces. This can be particularly beneficial in high-performance applications.
  • Debugging: With Result Monads, errors are handled explicitly, making it easier to pinpoint where and why a failure occurred, thus simplifying debugging.

Conclusion

By moving from exceptions to the Result Monad, developers can achieve better code readability, improved performance, and more predictable error handling. The Result Monad reduces the cognitive load required to understand and manage failure states, making for cleaner, more robust code. So next time you're faced with the choice, consider opting for the Result Monad and experience the benefits firsthand.

Have a goat day 🐐

Github : https://github.com/goatreview/ExceptionVsResult

Associated resources :

https://goatreview.com/mediatr-quickly-test-handlers-with-unit-tests/
https://goatreview.com/improving-error-handling-result-pattern-mediatr/



Join the conversation.

Great! Check your inbox and click the link
Great! Next, complete checkout for full access to Goat Review
Welcome back! You've successfully signed in
You've successfully subscribed to Goat Review
Success! Your account is fully activated, you now have access to all content
Success! Your billing info has been updated
Your billing was not updated