Table of Contents

Class Error

Namespace
Trellis
Assembly
Trellis.Core.dll

Closed discriminated union of Trellis error values. Domain-facing cases stay transport-neutral, while boundary-layer protocols can attach typed lower-level payloads through Error.TransportFault.

public abstract record Error : IEquatable<Error>
Inheritance
Error
Implements
Derived
Inherited Members
Extension Methods

Remarks

Closure. The base record has a private constructor; only nested cases declared in this file may inherit from Error. External code cannot extend the catalog, so switch over an Error reference is exhaustive at the language level.

Identity. Kind is a stable domain slug suitable for telemetry and wire serialization (e.g. "not-found"), fixed by the case. Code is the per-instance reason the producer names, and lives on the base so that every case carries it the same way: new Error.NotFound(resource) { Code = "account.closed" }. Cases whose reason is required take it as their first positional parameter instead.

Detail. Every case inherits an optional Detail property from the base. Callers supply it via object-initializer syntax: new Error.NotFound(resource) { Detail = "..." }. The boundary renderer prefers Detail when present; otherwise it computes a localized message from Kind, Code, and the typed payload.

Equality. Value-based equality over the discriminator, the typed payload, and Detail. Cause is intentionally excluded from equality so that two errors with identical surface payload compare equal regardless of how deeply they were wrapped — see the Equals(Error?) override for the rationale. Collection-bearing payloads use EquatableArray<T> for sequence equality.

Cause chain. Cause is a structured chain (never a live Exception). Cycles are detected at init time and throw InvalidOperationException.

Properties

Cause

Gets the optional structured cause of this error. Never holds a live Exception; use a child Error to attach causal context.

public Error? Cause { get; init; }

Property Value

Error

Code

Gets the machine-readable reason for this failure — the value a consumer sees, switches on, and carries from a bug report into a trace query. Defaults to Unspecified when the producer names no reason.

public string Code { get; init; }

Property Value

string

Remarks

The default is the sentinel meaning "no finer reason available", never Kind. A code that fell back to the kind could not be told apart from a producer that deliberately chose that string, and publishing it invited a consumer to branch on "not-found" as though someone had meant it. Defaulting to the sentinel makes that mistake unreachable rather than merely discouraged: the kind is not in this member for any boundary to leak.

This is the only code member, and it is storage rather than a per-case computed property. Two members is how an HTTP body and a span tag come to disagree about the same failure; two spellings of one member — a per-case ReasonCode payload behind a virtual Code — is how three cases came to have no way of carrying a reason at all.

Cases whose reason is required (Error.InvariantViolation, Error.Conflict, Error.Unexpected, Error.Forbidden) take it as their first positional parameter. Every assigned code must be non-blank, including assignments through a with expression. Other cases may leave the default unspecified sentinel; the Error.NotFound and Error.Gone factories also normalize blank optional code arguments to that sentinel.

Exceptions

ArgumentException

The assigned code is null, empty, or whitespace.

Detail

Gets the optional human-readable detail. When non-null the boundary renderer prefers this over the default template for Code.

public string? Detail { get; init; }

Property Value

string

Kind

Gets the stable domain slug for this case (e.g. "not-found", "invalid-input"). Suitable for telemetry, observability dimensions, and as the durable identifier that boundary layers translate into transport-specific type identifiers.

public abstract string Kind { get; }

Property Value

string

Methods

Equals(Error?)

Value equality over the discriminator (Trellis.Error.EqualityContract), Code, and Detail, plus each derived case's positional payload. Cause is intentionally excluded from equality and hashing — two errors with identical kind, payload, and detail represent the same logical failure regardless of how deeply they were wrapped. This mirrors Exception, whose equality does not recurse into InnerException, and keeps test assertions ergonomic (callers assert on the surface error without reconstructing the entire causal chain).

public virtual bool Equals(Error? other)

Parameters

other Error

Returns

bool

Remarks

How per-derived payload comparison works: this override deliberately checks only the members declared on the base — EqualityContract, Code, and Detail. Each derived sealed record (e.g., Error.NotFound, Error.InvalidInput) gets a compiler-generated Equals(Derived?) of the form base.Equals(other) && Field1 == other.Field1 && .... The base.Equals(other) call dispatches virtually to this override, contributing the kind+code+detail check; the derived method then ANDs in its per-property comparison. The net effect is element-wise equality across both base and derived fields, without any per-derived override needed. Code must be named here precisely because it is declared on the base: a derived record's generated Equals compares only its own members, so a code omitted from this override would let two errors with different reasons compare equal — and therefore collide in a cache key or a deduplicated log.

GetHashCode() uses the same compose-with-derived pattern: the override hashes EqualityContract, Code, and Detail, and each derived record's auto-generated GetHashCode combines base.GetHashCode() with hashes of its own properties.

GetDisplayMessage()

Returns a human-readable message suitable for logging, tracing, and diagnostic surfaces. Prefers the explicit Detail when set; otherwise flattens any per-field violation messages (for Error.InvalidInput) before falling back to Code.

public virtual string GetDisplayMessage()

Returns

string

GetHashCode()

Serves as the default hash function.

public override int GetHashCode()

Returns

int

A hash code for the current object.

ToString()

Returns a string that represents the current object.

public override string ToString()

Returns

string

A string that represents the current object.