-
Notifications
You must be signed in to change notification settings - Fork 0
Error Handling
ResultR provides consistent error handling through the Result<T> type. This page covers how errors are handled and best practices for error management.
ResultR automatically catches exceptions in your handlers and converts them to failure results:
public async ValueTask<Result<User>> HandleAsync(
GetUserRequest request,
CancellationToken cancellationToken)
{
// If this throws an exception...
var user = await _repository.GetByIdAsync(request.Id, cancellationToken);
return Result<User>.Success(user);
}
// ...ResultR catches it and returns:
// Result<User>.Failure(exception.Message, exception)| Exception Type | Behavior |
|---|---|
OperationCanceledException |
Re-thrown (allows proper cancellation handling) |
| All other exceptions |
Caught and converted to Result.Failure
|
var result = await _dispatcher.Dispatch(request);
if (result.IsFailure)
{
Console.WriteLine($"Error: {result.Error}");
if (result.Exception is not null)
{
// Log the full exception
_logger.LogError(result.Exception, "Operation failed: {Error}", result.Error);
// Check exception type
if (result.Exception is SqlException sqlEx)
{
// Handle database-specific error
}
}
}For expected failures (not exceptional conditions), return explicit failure results:
public async ValueTask<Result<User>> HandleAsync(
GetUserRequest request,
CancellationToken cancellationToken)
{
var user = await _repository.GetByIdAsync(request.Id, cancellationToken);
// Expected case: user not found
if (user is null)
return Result<User>.Failure($"User {request.Id} not found");
// Expected case: user is inactive
if (!user.IsActive)
return Result<User>.Failure("User account is deactivated");
return Result<User>.Success(user);
}Use ValidateAsync for input validation:
public ValueTask<Result> ValidateAsync(CreateUserRequest request)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(request.Email))
errors.Add("Email is required");
else if (!IsValidEmail(request.Email))
errors.Add("Email format is invalid");
if (string.IsNullOrWhiteSpace(request.Name))
errors.Add("Name is required");
else if (request.Name.Length > 100)
errors.Add("Name cannot exceed 100 characters");
return errors.Count > 0
? new(Result.Failure(string.Join("; ", errors)))
: new(Result.Success());
}[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
var result = await _dispatcher.Dispatch(new GetUserRequest(id));
if (result.IsSuccess)
return Ok(result.Value);
// Map errors to appropriate HTTP status codes
return result.Error switch
{
var e when e?.Contains("not found") == true => NotFound(new { error = result.Error }),
var e when e?.Contains("unauthorized") == true => Unauthorized(new { error = result.Error }),
_ => BadRequest(new { error = result.Error })
};
}Create a helper for consistent error responses:
public static class ResultExtensions
{
public static IActionResult ToActionResult<T>(this Result<T> result)
{
if (result.IsSuccess)
return new OkObjectResult(result.Value);
var problemDetails = new ProblemDetails
{
Title = "Operation Failed",
Detail = result.Error,
Status = StatusCodes.Status400BadRequest
};
return new ObjectResult(problemDetails)
{
StatusCode = StatusCodes.Status400BadRequest
};
}
}
// Usage
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
var result = await _dispatcher.Dispatch(new GetUserRequest(id));
return result.ToActionResult();
}Handle errors when chaining multiple operations:
public async ValueTask<Result<OrderConfirmation>> HandleAsync(
PlaceOrderRequest request,
CancellationToken cancellationToken)
{
// Step 1: Validate inventory
var inventoryResult = await _dispatcher.Dispatch(
new CheckInventoryRequest(request.Items), cancellationToken);
if (inventoryResult.IsFailure)
return Result<OrderConfirmation>.Failure($"Inventory check failed: {inventoryResult.Error}");
// Step 2: Create order
var orderResult = await _dispatcher.Dispatch(
new CreateOrderRequest(request.CustomerId, request.Items), cancellationToken);
if (orderResult.IsFailure)
return Result<OrderConfirmation>.Failure($"Order creation failed: {orderResult.Error}");
// Step 3: Process payment
var paymentResult = await _dispatcher.Dispatch(
new ProcessPaymentRequest(orderResult.Value.Id, request.Payment), cancellationToken);
if (paymentResult.IsFailure)
{
// Compensating action: cancel the order
await _dispatcher.Dispatch(new CancelOrderRequest(orderResult.Value.Id), cancellationToken);
return Result<OrderConfirmation>.Failure($"Payment failed: {paymentResult.Error}");
}
return Result<OrderConfirmation>.Success(
new OrderConfirmation(orderResult.Value, paymentResult.Value));
}Add context to errors using metadata:
public async ValueTask<Result<Order>> HandleAsync(
CreateOrderRequest request,
CancellationToken cancellationToken)
{
try
{
var order = await _repository.CreateAsync(request, cancellationToken);
return Result<Order>.Success(order);
}
catch (Exception ex)
{
return Result<Order>.Failure("Failed to create order", ex)
.WithMetadata("CustomerId", request.CustomerId)
.WithMetadata("ItemCount", request.Items.Count)
.WithMetadata("Timestamp", DateTime.UtcNow);
}
}Retrieve metadata values with type safety using GetMetadataValueOrDefault<TValue>:
if (result.IsFailure)
{
// Type-safe retrieval - returns default if key missing or type mismatch
var customerId = result.GetMetadataValueOrDefault<int>("CustomerId");
var itemCount = result.GetMetadataValueOrDefault<int>("ItemCount");
var timestamp = result.GetMetadataValueOrDefault<DateTime>("Timestamp");
_logger.LogError(
"Order creation failed for customer {CustomerId} with {ItemCount} items at {Timestamp}",
customerId, itemCount, timestamp);
}Use AfterHandleAsync for consistent error logging:
public ValueTask AfterHandleAsync(MyRequest request, Result<MyResponse> result)
{
if (result.IsFailure)
{
_logger.LogError(
result.Exception,
"Request {RequestType} failed: {Error}. Request: {@Request}",
typeof(MyRequest).Name,
result.Error,
request);
}
return default;
}-
Use explicit failures for expected conditions - Don't throw exceptions for "user not found" or "invalid input"
-
Reserve exceptions for unexpected conditions - Database connection failures, network errors, etc.
-
Include context in error messages - "User 123 not found" is better than "Not found"
-
Log exceptions, not just error messages - The exception contains valuable stack trace information
-
Don't swallow exceptions silently - Always log or report them somewhere
-
Use validation for input errors -
ValidateAsyncprovides a clean separation
- Best Practices - Recommended patterns and conventions
Built with ❤️ for the C# / DotNet community.