Introduction
Error handling is an essential part of the software development process. In a previous article, I introduced how to handle exceptions in .NET applications. Although using custom exceptions that inherit from the Exception class is a powerful way to manage errors, throwing exceptions is a resource-intensive operation because it requires collecting the stack trace, which can impact program performance to some extent.
To address this issue, we can apply a different approach by dividing errors into two groups:
- Manageable errors: Errors that we handle explicitly and return results within the program logic.
- Uncontrollable exceptions: Unexpected issues, such as database connection failures, that require using exceptions.
With this approach, we will manage errors and exceptions independently, improving performance by minimizing unnecessary exception throwing. The Result Pattern was created to help us achieve this. Let’s explore how to implement this pattern!
What is the Result Pattern?
The Result Pattern is a design approach that encapsulates the success and failure states of an operation into a single, type-safe object. Instead of using exceptions to propagate errors, this pattern returns a Result object to clearly indicate the outcome of an operation.
A Result object typically contains:
- A value (for successful operations).
- An error type or error message (for failed operations).
Steps to Implement the Result Pattern in Our Application
There are several ways to implement the Result Pattern in a .NET application, but they all fundamentally follow the same steps. Let’s continue reading to see what the steps are to implement the Result Pattern. Don’t forget to open your Editor and try it out after reading so you can apply and extend the approach for your own application!
- Create an ErrorType Enum
This enum will represent the different types of errors in the application. Personally, I find that program errors can be categorized into these 5 types. If you want to customize with any specific errors that you frequently use in your application, feel free to add them to this Enum.
public enum ErrorType
{
Validation,
NotFound,
Unauthorized,
Conflict,
Unknown
}
- Create an Error Class
This class contains information about the error, such as its type and message. There are many ways to implement this class — it could be an Abstract class containing the detailed error information you want to expose. You could also use a Record for implementation. However, in this article, I will use a regular class with just two fields, Type and Message, to keep things simple for illustration. (In real projects, I also return additional information such as TraceId, along with details like LineNumber, CallerMethod, FilePath, and Source to support logging.)
public class Error
{
public ErrorType Type { get; }
public string Message { get; }
public Error(ErrorType type, string message)
{
Type = type;
Message = message;
}
public override string ToString() => $"{Type}: {Message}";
}
- Create and Handle Logic in the Result Class
This class represents the result of an operation, including success or failure information. This class is typically used for actions where you only want to know whether the operation succeeded or failed, without caring about the return value on success. For example, Commands that only need to know whether execution succeeded or failed.
public class Result
{
public bool IsSuccess { get; }
public Error? Error { get; }
protected Result(bool isSuccess, Error? error)
{
IsSuccess = isSuccess;
Error = error;
}
public static Result Success() => new Result(true, null);
public static Result Failure(Error error) => new Result(false, error);
}
- Create and Handle the Generic Result Class
The generic version of Result allows returning a value on success. With this version, when you need to care about the return value, such as querying data from a database, use the Generic class.
public class Result<T> : Result
{
public T? Value { get; }
private Result(T value) : base(true, null)
{
Value = value;
}
private Result(Error error) : base(false, error)
{
Value = default;
}
public static Result<T> Success(T value) => new Result<T>(value);
public static Result<T> Failure(Error error) => new Result<T>(error);
}
- Create Extension Methods to Handle Result Matching
Extension methods help handle Results in a concise, seamless, and consistent manner.
public static class ResultExtensions
{
public static void Match(
this Result result,
Action onSuccess,
Action<Error> onFailure)
{
if (result.IsSuccess)
onSuccess();
else
onFailure(result.Error!);
}
public static void Match<T>(
this Result<T> result,
Action<T> onSuccess,
Action<Error> onFailure)
{
if (result.IsSuccess)
onSuccess(result.Value!);
else
onFailure(result.Error!);
}
}
- Bonus: Create Extension Methods for Consistent Error Responses
For .NET WebApi applications, we typically return ActionResults. Ensuring consistent error codes and responses across all endpoints is important. We will create an Extension Method to handle this, while also avoiding repetitive code in each endpoint inside the Controller.
public static class ApiResponseExtensions
{
public static IActionResult ToActionResult(this Result result)
{
if (result.IsSuccess)
return new OkResult();
return new ObjectResult(result.Error)
{
StatusCode = result.Error!.Type switch
{
ErrorType.Validation => 400,
ErrorType.NotFound => 404,
ErrorType.Unauthorized => 401,
_ => 500
}
};
}
public static IActionResult ToActionResult<T>(this Result<T> result)
{
if (result.IsSuccess)
return new OkObjectResult(result.Value);
return result.ToActionResult();
}
}
Usage Example
Below is a sample code snippet to help you visualize how to use the Result Pattern. Here, it is simply a UserService and an API that retrieves data through the UserService. You can fully extend this for your own application, whether using Repository, CQRS, or other patterns — they can all incorporate the Result Pattern.
public class UserService
{
public Result<User> GetUserById(int id)
{
if (id <= 0)
return Result<User>.Failure(new Error(ErrorType.Validation, "ID người dùng không hợp lệ"));
var user = GetUserFromDatabase(id);
return user is not null
? Result<User>.Success(user)
: Result<User>.Failure(new Error(ErrorType.NotFound, "Người dùng không tồn tại"));
}
private User? GetUserFromDatabase(int id)
{
// Giả lập lấy dữ liệu từ database
return id == 1 ? new User { Id = 1, Name = "John Doe" } : null;
}
}
// Ví dụ trong Controller
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
private readonly UserService _userService = new();
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var result = _userService.GetUserById(id);
return result.ToActionResult();
}
}
Conclusion
The Result Pattern provides a clear and structured approach to error handling in .NET applications, reducing reliance on exceptions for manageable errors. This approach not only improves performance but also helps clearly distinguish between recoverable errors and unexpected exceptions. I hope that through this article, you can easily integrate this pattern into your application for more effective error handling.
Thank for reading!!!
