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 a400
on the first type of error and with a404
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 astring
and not, say, anint
becausestring
s 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 string
s.
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 Result
s into HTTP codes exactly the same way, so a BaseController
class works best here.
4. Summary
To differentiate 404
s from 400
s:
-
Create an explicit
Error
class, -
Use
Result
in combination with that class instead ofstring
s. -
Convert
Result
s into HTTP response codes inBaseController
.
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).
6. Other articles in the series
-
Advanced error handling techniques (this post)
-
Combining ASP.NET Core validation attributes with Value Objects
- ← You are naming your tests wrong!
- Combining ASP.NET Core validation attributes with Value Objects →
Subscribe
Comments
comments powered by Disqus