DEV Community

Cover image for Kentico Xperience Design Patterns: Handling Failures - Return Errors, Don't Throw Them
Sean G. Wright
Sean G. Wright

Posted on • Updated on

Kentico Xperience Design Patterns: Handling Failures - Return Errors, Don't Throw Them

In a previous post we looked at how to centralize exception handling in a Kentico Xperience ASP.NET Core application by using out-of-the-box middleware. This enabled us to handle errors as a cross-cutting concern and DRY up our code ๐Ÿ‘๐Ÿฝ.

However, the approach had some drawbacks. Namely, the method signatures of our code became dishonest - we had to assume that any method could throw an exception ๐Ÿ˜’.

Centralized exception handling, while not bad in itself, can encourage using exception handling as control flow, which can make code much harder to reason about when reading it. This handling also tends to occur in an area of our application where it's too "late" to gracefully respond to the failure, so the best we can do is log it an display a generic error page ๐Ÿ˜”.

Below we'll look at an alternative to throwing exceptions - returning errors from methods ๐Ÿ˜ฎ.

Note: This is part 2 of a 3 part series on handling failures.

Part 1 and Part 3

๐Ÿ“š What Will We Learn?

What Do We Do With A Failure?

One important question to ask, before we proceed further, is "What do we do with a failure?"

In some applications the answer is simple - nothing! We will try to catch Exceptions in a central place in the application, log them to the Xperience Event Log, and let middleware render an error page (YOLO! ๐Ÿค˜๐Ÿพ).

However, in more complex situations we might need to handle errors at the method call site so we could provide fallback operations or data to ensure as much of a Page's content is rendered as possible.

If we are using Exceptions to communicate failures, we're going to end up with try/catch logic scattered around each of these method call sites, which is why we adopted centralized error handling in the previous post ๐Ÿคฆ๐Ÿปโ€โ™€๏ธ.

The alternative to this try/catch spaghetti is to "return" the error to the caller, handling any exceptions or failures within the method being called ๐Ÿคจ.

Prior Art: Go

We can look to other languages for inspiration here, since some of them, like Go and Rust, treat exceptions as truly exceptional events and instead return errors from functions when there are logic violations and other failures ๐Ÿค“.

Go treats string values as the standard error type that a function might return in case of a failure, so it's common to see a function defined as follows:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, fmt.Errorf("square root of negative number %g", f)
    }
    // implementation

    return val, nil
}
Enter fullscreen mode Exit fullscreen mode

In Go, the function return type ((float64, error)) comes after the parameter list. In this case, it shows Sqrt will always return 2 values (which is sometimes called a couple or a tuple). The first item returned is "value" and the second is the "error".

Looking at the function body, we can see the values we return when the input is invalid (f < 0):

  • 0 - the results of the square root operation
  • fmt.Errorf(...) which returns a string (as the type error)

The value is a "default" and the error is a helpful message ๐Ÿ˜‰.

In the successful case at the end of the function we return val as the calculated value and nil representing no errors.

Any code that would call Sqrt would check if any errors had been returned before processing the result:

f, err := Sqrt(-1)

if err != nil {
    fmt.Println(err)
}
Enter fullscreen mode Exit fullscreen mode

Even though Go treats string values as the standard error type, developers can create their own custom errors that contain more context. Callers of a function can then react to this contextual error information ๐Ÿง.

Every function that can result in a failure follows this pattern, which means a typical Go program has lots of if err != nil { ... } blocks in it. This might lead us to ask ourselves, "What's the benefit over exceptions if we still have to repeat guards/checks everywhere?" ๐Ÿ˜•

Well, the main benefit is that the function signatures tell the call exactly what to expect. There's no surprises here! ๐Ÿ‘๐Ÿฟ

If your application can't ever produce errors, then don't worry, you won't need many error returning functions. If it can experience experience errors then they should be modeled and handled ๐Ÿ‘๐Ÿผ.

โš  Don't confuse an application that doesn't handle errors with an application that doesn't have them. โš 

Returning Errors

Let's look at an example scenario for a Kentico Xperience application.

In a HomeController that builds a view model for our Home Page, we need to retrieve content from the database, which we will do with a HomePageService class:

public class HomeController : Controller
{
    private readonly HomePageService service;
    private readonly IEventLogService log;

    // ...

    public async Task<ActionResult> Index(CancellationToken token)
    {
        ??? = service.GetData();

        return View(new HomeViewModel(???));
    }
}
Enter fullscreen mode Exit fullscreen mode

Before we call the HomePageService, we need to figure out how to design the GetData method so that we don't have to worry about catching exceptions.

First, we'll define the method signature to return a C# Tuple, which is very similar to Go's multiple-value returns for functions that can have failures.

Tuples provide "concise syntax to group multiple data elements in a lightweight data structure".

public class HomePageService
{
    private readonly IPageRetriever pageRetriever;
    private readonly IPageUrlRetriever urlRetriever;
    private readonly IPageDataContextRetriever contextRetriever;

    // ...

    public async Task<(HomePageData? data, string? error)> GetData(CancellationToken token)
    {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

Now for the implementation of our GetData method:

public async Task<(HomePageData? data, string? error)> GetData(CancellationToken token)
{
    if (!retriever.TryRetrieve<HomePage>(out var context))
    {
        return (null, "Could not get HomePage Page context");
    }

    var products = Enumerable.Empty<ProductPage>();

    try 
    {
        products = await pageRetriever<ProductPage>
            .RetrieveAsync(
                q => q.WhereTrue("ProductPageIsFeatured"),
                b => b.Key("HomePageFeaturedProducts"),
                cancellationToken: token);
    }
    catch (Exception ex)
    {
        return (null, $"Query for featured products failed: {ex}");
    }

    var featured = new List<FeaturedProduct>();

    foreach (var product in products)
    {
        var url = urlRetriever.Retrieve(product);

        if (url is null)
        {
            continue;
        }

        featured.Add(new FeaturedProduct(product, url));
    }

    return (new HomePageData(products, context.Page), null);       
}
Enter fullscreen mode Exit fullscreen mode

The caller (HomeController.Index) can now decide what to do if there are any errors present in the returned Tuple:

public async Task<ActionResult> Index(CancellationToken token)
{
    var (data, error) = await service.GetData();

    if (error is not null or data is null)
    {
        log.LogError(
           "HomeController",
           "DATA_FAILURE",
           error);

        return ServerError(500);
    }

    return View(new HomeViewModel(data));
}
Enter fullscreen mode Exit fullscreen mode

Better Method Signatures

If the C# Tuple seems a little awkward, we could instead try a C# 10 record struct:

We choose a record struct because it has all the low memory benefits of struct types and the auto-generated members of record types while also having a very simple definition ๐Ÿ˜.

public readonly record struct Result<T>(T? Value, string? Error)
{
    public static Result<T> Value(T value) => 
        new Result<T>(value, null);
    public static Result<T> Error(string error) => 
        new Result<T>(null, error);
}
Enter fullscreen mode Exit fullscreen mode

This type is pretty simple, but that's really all we want (for now) if we are trying to return errors but avoid Tuples.

We can update our HomePageService.GetData() method to the following:

public async Task<Result<HomePageData>> GetData(CancellationToken token)
{
    if (!retriever.TryRetrieve<HomePage>(out var context))
    {
        return Result<HomePageData>.Error(
            "Could not get HomePage Page context");
    }

    var products = Enumerable.Empty<ProductPage>();

    try 
    {
        products = await pageRetriever<ProductPage>
            .RetrieveAsync(
                q => q.WhereTrue("ProductPageIsFeatured"),
                b => b.Key("HomePageFeaturedProducts"),
                cancellationToken: token);
    }
    catch (Exception ex)
    {
        return Result.Error<HomePageData>(
            $"Query for featured products failed: {ex}");
    }

    var featured = new List<FeaturedProduct>();

    foreach (var product in products)
    {
        var url = urlRetriever.Retrieve(product);

        if (url is null)
        {
            continue;
        }

        featured.Add(new FeaturedProduct(product, url));
    }

    var data = new HomePageData(products, context.Page);
    return Result<HomePageData>.Value(data); 
}
Enter fullscreen mode Exit fullscreen mode

We still have to make the same checks in our HomeController, but at least the method signature of our service has been cleaned up ๐Ÿงน and the creation of "values" and "errors" returns from our method is more explicit ๐Ÿ’ช.

record struct types include a Deconstruct method which adds the deconstruction syntax that Tuples have. This means our custom Result type can use the same syntax as the tuple in our HomeController.

Conclusion

We've reviewed the problems that come with handling errors in our application with only global exception handling. We end up with dishonest method signatures that we can't trust and cannot respond to errors where we need to ๐Ÿ˜‘.

By following the pattern of returning errors, like the programming languages Go and Rust, we end up with a much more understandable and readable code base. We encourage modeling the potential error states of our application, giving ourselves the opportunity to handle them ๐Ÿ˜„.

While the C# tuple syntax has its uses, using it to model application errors might not be the best. So instead we can create a simple container type - Result - to make our method signatures more legible ๐Ÿ˜€. Unfortunately this still leaves us with a bunch of procedural if (error is not null ...) { ... } checks in our application ๐Ÿ˜ฃ.

In my next post, we'll look at a way of returning failures so we can respond to them intelligently, while avoiding both complex method signatures and procedural conditional checks everywhere by using the Result monad ๐Ÿค”.
...

As always, thanks for reading ๐Ÿ™!

References


Photo by Jordan Madrid on Unsplash

We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.

#kentico

#xperience

Or my Kentico Xperience blog series, like:

Top comments (0)