I've got some news that might revolutionize how you handle errors and control your application flow in C#.

Have you ever felt that traditional error-handling methods and libraries don’t cut it anymore? Meet ErrorOr, a C# library designed to simplify and streamline error handling with a unified approach.

ErrorOr is a C# library designed to streamline error handling by offering a fluent and intuitive interface to represent a successful result or an error. Rather than dealing with exceptions or complex conditional structures, developers can use ErrorOr to encapsulate both outcomes in a single, unified object.

Key Features

  1. Fluent API: Easily represent results or errors using intuitive methods and conversions.
  2. Built-in Error Types: Categorize errors using predefined types such as Failure, Unexpected, Validation, and more.
  3. Custom Error Types: Create error categories tailored to your application's needs.
  4. Seamless Integration: Works hand-in-hand with popular libraries like FluentValidation.
  5. Unified Error Handling: Simplify error-checking logic and enhance code readability.

Exception flow vs. ErrorOr flow

The side-by-side comparison below illustrates how ErrorOr provides a more structured and intuitive approach to error handling, moving away from the pitfalls of exception-based control flow.

Exception flow

User GetUser(Guid id = default)
{
    if (id == default)
    {
        throw new ValidationException("Id is required");
    }

    return new User(Name: "Mario Smolcic");
}

try
{
    var user = GetUser();
    Console.WriteLine(user.Name);
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

ErrorOr flow

ErrorOr<User> GetUser(Guid id = default)
{
    if (id == default)
    {
        return Error.Validation("Id is required");
    }

    return new User(Name: "Mario Smolcic");
}

var errorOrUser = GetUser();
errorOrUser.SwitchFirst(
    user => Console.WriteLine(user.Name),
    error => Console.WriteLine(error.Description));

Predefined and Custom Error Types

public static Error Error.Failure(string code, string description);
public static Error Error.Unexpected(string code, string description);
public static Error Error.Validation(string code, string description);
public static Error Error.Conflict(string code, string description);
public static Error Error.NotFound(string code, string description);

public static class AuthorizationErrorTypes
{
    public const int Unauthorized = 401;
}

public static class Errors
{
    public static Error NotFound => Error.NotFound(
        "Entity.NotFound",
        "Entity not found.");
    
    public static Error Unauthorized => Error.Custom(
        AuthorizationErrorTypes.Unauthorized,
        "Authorization.Unauthorized",
        "Unauthorized.");
}

ErrorOr not only comes with predefined error types for common scenarios but also empowers you to define custom error categories tailored to your application's specific needs.

Built-in Result Types

ErrorOr<Success> result = Result.Success;
ErrorOr<Created> result = Result.Created;
ErrorOr<Updated> result = Result.Updated;
ErrorOr<Deleted> result = Result.Deleted;

// Example
ErrorOr<Deleted> DeleteUser(Guid id)
{
    var user = await _userRepository.GetByIdAsync(id);
  	if (user is null)
    {
        return Errors.NotFound;
    }

    await _userRepository.DeleteAsync(user);
    return Result.Deleted;
}

The library also contains the usual CRUD operation result types. You can embrace its simplicity without compromising the granularity of the response.

Endpoint example

protected IActionResult HandleErrors(List<Error> errors)
{
    if (errors.Any(e => e.NumericType == AuthorizationErrorTypes.Unauthorized))
    {
        return Unauthorized();
    }

    if (errors.Any(e => e.Type == ErrorType.Unexpected))
    {
        return Problem();
    }

    if (errors.All(e => e.Type == ErrorType.Validation))
    {
        var modelStateDictionary = new ModelStateDictionary();

        foreach (var error in errors)
        {
            modelStateDictionary.AddModelError(error.Code, error.Description);
        }

        return ValidationProblem(modelStateDictionary);
    }

    var firstError = errors.First();
    var statusCode = firstError.Type switch
    {
        ErrorType.NotFound => StatusCodes.Status404NotFound,
        ErrorType.Validation => StatusCodes.Status400BadRequest,
        ErrorType.Conflict => StatusCodes.Status409Conflict,
        _ => StatusCodes.Status500InternalServerError
    };

    return Problem(statusCode: statusCode, title: firstError.Description);
}

[HttpGet("{id:guid}")]
public async Task<IActionResult> GetUser(Guid Id)
{
    var getUserQuery = new GetUserQuery(Id);

    ErrorOr<User> getUserResponse = await _mediator.Send(getUserQuery);

    return getUserResponse.Match(
        user => Ok(_mapper.Map<UserResponse>(user)),
        errors => HandleErrors);
}
  1. Error Representation: The HandleErrors method accepts a list of errors. An appropriate HTTP response is generated based on the nature of these errors (like authorization issues, unexpected errors, or validation problems).
  2. Functional Handling: The GetUser method showcases a functional paradigm. By using the ErrorOr<User> type, which is reminiscent of the Either monad in functional languages, the method can elegantly swap between the success and failure scenarios.
  3. Expressiveness: Using methods like Match simplifies error handling, making the code more readable and maintainable.

Using FluentValidation and MediatR? No problem!

There's a smooth way to integrate ErrorOr in the regular FluentValidation flow with MediatR and return the ErrorOr result explicitly instead of throwing an error as usual. Let's take a look:

public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
    where TResponse : IErrorOr
{
    private readonly IValidator<TRequest>? _validator;

    public ValidationBehavior(IValidator<TRequest>? validator = null)
    {
        _validator = validator;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (_validator == null)
        {
            return await next();
        }

        var validationResult = await _validator.ValidateAsync(request, cancellationToken);

        if (validationResult.IsValid)
        {
            return await next();
        }

        return TryCreateResponseFromErrors(validationResult.Errors, out var response)
            ? response
            : throw new ValidationException(validationResult.Errors);
    }

    private static bool TryCreateResponseFromErrors(List<ValidationFailure> validationFailures, out TResponse response)
    {
        List<Error> errors = validationFailures.ConvertAll(x => Error.Validation(
                code: x.PropertyName,
                description: x.ErrorMessage));

        response = (TResponse?)typeof(TResponse)
            .GetMethod(
                name: nameof(ErrorOr<object>.From),
                bindingAttr: BindingFlags.Static | BindingFlags.Public,
                types: new[] { typeof(List<Error>) })?
            .Invoke(null, new[] { errors })!;

        return response is not null;
    }
}
  1. Avoid Exceptions for Control Flow: Instead of using exceptions to manage the control flow, return an error response. This aligns with the idea of treating exceptions as "exceptional" scenarios.
  2. Reflection for Dynamic Creation: The TryCreateResponseFromErrors method demonstrates a neat trick to create a response dynamically using reflection. This ensures our ValidationBehavior remains generic and can work with any response type that conforms to our IErrorOr pattern.
  3. Integration with FluentValidation: The behavior integrates smoothly with the FluentValidation library. If a request isn't valid, it attempts to generate an error response. If it can't, it defaults to throwing a validation exception.

In Conclusion

The world of error handling in C# has seen its fair share of practices. From the age-old try-catch blocks to throwing specific exceptions, we've always looked for more streamlined, efficient methods. The ErrorOr library brings to the table an evolved, intuitive approach, bridging the gap between traditional error handling and the demands of modern application development. By embracing this library, you can ensure not just cleaner code but also a consistent error-handling strategy that integrates seamlessly with popular tools like FluentValidation and MediatR.

As technology evolves, so should our methodologies. Instead of relying on exceptions as a primary means of control flow, explore the capabilities of ErrorOr. It could be a breath of fresh air for your codebase, aligning it more with functional programming paradigms and improving readability and maintainability.

The library source code can be found here.