Skip to content

Errors

Unlike a single Error record with an ErrorType enum, Haitch.Unions models each kind of failure as its own record type deriving from Error.

public record Error(string Code, string Description)
PropertyTypeDescription
CodestringA stable, machine-readable identifier, e.g. "user.not_found".
DescriptionstringA human-readable explanation.

Both are required: the constructor throws ArgumentNullException if either is null.

Each kind is a record deriving from Error — construct it directly, there are no Error.NotFound(...)-style factories.

TypeUse case
NotFoundErrorA requested resource does not exist.
ValidationErrorThe input was invalid. Carries Failures.
ConflictErrorThe operation conflicts with the current state.
UnauthorizedErrorThe caller is not authenticated.
ForbiddenErrorThe caller is not allowed to perform the operation.
UnexpectedErrorAn unexpected error occurred. Carries an optional Exception.
new NotFoundError("user.not_found", $"No user with id {id}.");
new ConflictError("user.duplicate", "A user with that email already exists.");
new UnauthorizedError("auth.missing", "No credentials provided.");
new ForbiddenError("auth.denied", "You do not have access.");
new UnexpectedError("op.failed", "Something went wrong.", exception);

Return a plain Error for a generic, uncategorised failure that doesn’t fit one of the six kinds.

public record ValidationError(
string Code,
string Description,
IReadOnlyDictionary<string, string[]>? Failures = null
) : Error(Code, Description)

Failures maps a field name to its failure messages; a null argument becomes an empty dictionary.

var error = new ValidationError(
"user.invalid",
"One or more fields are invalid",
new Dictionary<string, string[]> { ["email"] = ["Email is required."] });

ValidationError.Merge combines several validation errors into one, joining Failures by field:

var merged = ValidationError.Merge(emailError, nameError);
// Or with a custom code and description
var merged2 = ValidationError.Merge(
"user.invalid",
"One or more fields are invalid",
emailError,
nameError);

Result.Combine has an overload that merges every error instead of stopping at the first. ValidationError.Merge(IReadOnlyList<Error>) fits it as a method group: it returns the first non-validation error if any of the results failed with something other than a ValidationError, otherwise every validation error merged with the default code and description.

Result<IReadOnlyList<Order>> orders = Result.Combine(results, ValidationError.Merge);

Error is an open record, so you can add your own kinds alongside the six built in:

public record RateLimitedError(string Code, string Description, TimeSpan RetryAfter)
: Error(Code, Description);