Advanced error handling techniques

This post describes some (more) advanced error handling techniques. I’ll probably do a series because this topic is large and there’s quite a few things that need to be cleared out, but we’ll see.

This first article in the series is about organizing errors in your application. It’s also an answer to a question I received 3 times from different people during the past couple of weeks.

Here’s an aggregate version of that question:

How can I go about responding with different HTTP status codes depending on the nature of the failure without API (UI) logic leaking into the domain layer? In particular, how can I differentiate between a Result that failed because of a validation error and another one that failed because the entity was not found? I’d like to respond with a 400 on the first type of error and with a 404 on the second.

I’ll lay out a typical setup to illustrate where this question comes from. Let’s say we have a student accounting software and need to implement an API endpoint that updates a student’s name and email. Let’s also say that we want to introduce two layers:

  • Controllers that handle ASP.NET plumbing, such as routing and working with data contracts.

  • Application services that sit one layer below controllers. They deal with application logic: coordinate the work between out-of-process dependencies and the domain model.

Here’s how such a controller may look:

[Route("api/students")]
public sealed class StudentController : Controller
{
    private readonly StudentApplicationService _studentService;

    public StudentController(StudentApplicationService studentService)
    {
        _studentService = studentService;
    }

    [HttpPut("{id}")]
    public IActionResult EditPersonalInfo(long id, [FromBody] StudentPersonalInfoDto dto)
    {
        Result result = _studentService.EditPersonalInfo(id, dto.Name, dto.Email);

        return result.IsSuccess ? (IActionResult)Ok() : BadRequest(result.Error);
    }
}

As you can see, this class just delegates the work to StudentApplicationService. Here’s this service itself:

public sealed class StudentApplicationService
{
    private readonly UnitOfWork _unitOfWork;

    public StudentApplicationService(UnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public Result EditPersonalInfo(long id, string name, string email)
    {
        Student student = _unitOfWork.Get<Student>(id);

        if (student == null)
            return Result.Failure($"No student found for Id '{id}'"); // should result in 404

        bool emailIsTaken = _unitOfWork
            .Query<Student>()
            .Any(x => x.Email == email && x.Id != id);

        if (emailIsTaken)
            return Result.Failure($"Email '{email}' is taken"); // should result in 400

        student.Name = name;
        student.Email = email;

        _unitOfWork.Commit();

        return Result.Ok();
    }
}

The problem with this code is that, because all errors are represented with plain strings, you can’t differentiate between different failed results. The controller translates both failures into a 400 response:

return result.IsSuccess
    ? (IActionResult)Ok() // 200
    : BadRequest(result.Error); // 400

Of course, in a real-world project, there’s not much value in having both StudentController and StudentApplicationService. You can just merge the service into the controller, and that would solve the problem. The controller can very well handle the responsibility of editing students' personal info on its own. And by doing so, that controller can also respond with different HTTP codes depending on the error; it doesn’t need to use the Result class. It’s only when you need to translate a Result into an HTTP status code do you start to have this problem.

There are situations where the separation between controllers and application services becomes useful, though. For example, you’ll have to segregate controllers from command handlers in CQRS (command handlers act as the application services layer in CQRS) if you want to introduce decorators. So, for the sake of argument, let’s just assume that the separation is justified in our example for some reason.

1. Meet the Error class

So how to solve this problem? How can you treat failed results differently depending on the failure?

You need to introduce an explicit Error class that would represent all errors in your application:

public sealed class Error : ValueObject
{
    public string Code { get; }
    public string Message { get; }

    internal Error(string code, string message)
    {
        Code = code;
        Message = message;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Code;
    }
}

There are two fields in this class:

  • Code is what defines an error. It’s a string and not, say, an int because strings are easier to work with as you can assign them meaningful descriptors.

  • Message is for additional debug information just for developers. If you pass errors to external systems, those systems shouldn’t ever bind to error messages, only to their codes.

Note that Error is a Value Object whose identity tied to the Code field. This underlines the insignificance of the Message field: two errors with the same code will be considered the same even if their messages differ. In scenarios with inter-application communication, you can change the message freely but should always keep the code intact for backward compatibility.

2. Enumerating all application errors

The introduction of the Error class allows you to do some interesting things. First, you can enumerate all possible errors in your application:

public static class Errors
{
    public static class Student
    {
        public static Error EmailIsTaken(string email) =>
            new Error("student.email.is.taken", $"Student email '{email}' is taken");

        /* other errors specific to students go here */
    }

    public static class General
    {
        public static Error NotFound(string entityName, long id) =>
            new Error("record.not.found", $"'{entityName}' not found for Id '{id}'");

        /* other general errors go here */
    }
}

Such explicitness gives the developers visibility into what errors there are in your application and in what circumstance they may be returned. Which is something you could have never achieved with plain strings.

Note the Student and General subclasses. They are useful for error categorization. If you don’t have many errors in your projects, you can just keep those errors in the parent Errors class. On the other hand, if you’ve got a lot of them, you can create even more subcategories by introducing classes inside Student and General.

After putting all errors into one place, it’s a good idea to also make sure you don’t accidentally reuse their codes. Here’s a test checking that codes in all errors are unique:

public sealed class ErrorTests
{
    [Fact]
    public void Error_codes_must_be_unique()
    {
        List<MethodInfo> methods = typeof(Error)
            .GetMethods(BindingFlags.Static | BindingFlags.Public)
            .Where(x => x.ReturnType == typeof(Error))
            .ToList();

        int numberOfUniqueCodes = methods.Select(x => GetErrorCode(x))
            .Distinct()
            .Count();

        numberOfUniqueCodes.Should().Be(methods.Count);
    }

    private string GetErrorCode(MethodInfo method)
    {
        object[] parameters = method.GetParameters()
            .Select<ParameterInfo, object>(x =>
            {
                if (x.ParameterType == typeof(string))
                    return string.Empty;

                if (x.ParameterType == typeof(long))
                    return 0;

                throw new Exception();
            })
            .ToArray();

        var error = (Error)method.Invoke(null, parameters);
        return error.Code;
    }
}

3. Converting errors into HTTP codes

Now that you’ve brought strong typing to application errors, you can specify explicit errors when returning a failed Result:

public sealed class StudentApplicationService
{
    private readonly UnitOfWork _unitOfWork;

    public StudentApplicationService(UnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public Result EditPersonalInfo(long id, string name, string email)
    {
        Student student = _unitOfWork.Get<Student>(id);

        if (student == null)
            return Result.Failure(Errors.General.NotFound("Student", id)); // should result in 404

        bool emailIsTaken = _unitOfWork
            .Query<Student>()
            .Any(x => x.Email == email && x.Id != id);

        if (emailIsTaken)
            return Result.Failure(Errors.Student.EmailIsTaken(email)); // should result in 400

        student.Name = name;
        student.Email = email;

        _unitOfWork.Commit();

        return Result.Ok();
    }
}

Note that there’s currently no Result<E> in the C# functional extensions library, this is in the works. For now, you will have to either use Result<T,E> or create your own version of Result.

Now you can treat these two errors differently in the controller. Or better yet, delegate this responsibility to a BaseController class:

public class BaseController : Controller
{
    protected new IActionResult Ok()
    {
        return base.Ok(Envelope.Ok());
    }

    protected IActionResult Ok<T>(T result)
    {
        return base.Ok(Envelope.Ok(result));
    }

    protected IActionResult FromResult(Result result)
    {
        if (result.IsSuccess)
            return Ok();

        if (result.Error == Errors.General.NotFound())
            return NotFound(Envelope.Error(result.Error));

        return BadRequest(Envelope.Error(result.Error));
    }
}

Viewers of my CQRS in practice course may also ask a reasonable question:

Should you create a decorator to handle the conversion from Result to HTTP codes?

No, you shouldn’t. Decorators are for behavior that differs depending on the API endpoint. Here, you want all your endpoints to convert Results into HTTP codes exactly the same way, so a BaseController class works best here.

4. Summary

To differentiate 404s from 400s:

  • Create an explicit Error class,

  • Use Result in combination with that class instead of strings.

  • Convert Results into HTTP response codes in BaseController.

5. Up next

In the next article of this series, I’ll show how to use the built-in ASP.NET’s validation mechanism (attributes) such that all domain logic is still concentrated inside your domain layer (in Value Objects).

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus