Elarion

Results & errors

Result<T> is a lightweight success-or-failure return type, and AppError is the transport-agnostic failure model.

Handlers return outcomes as values, not exceptions. Result<T> carries either a success value or an AppError, and the host maps that error to whatever the transport requires.

Result<T>

Result<T> is a readonly struct with a success value or an AppError:

public readonly struct Result<T> {
    public bool IsSuccess { get; }
    public T Value { get; }
    public AppError Error { get; }

    public static Result<T> Success(T value);
    public static Result<T> Failure(AppError error);
}

Implicit conversions let a handler return either a response or an error directly — no wrapping ceremony:

return new Response(project.Id);                       // implicit Success
return AppError.Validation("Name is required.");       // implicit Failure
return AppError.Conflict("Invoice number is already reserved.");

Because it is a struct with ValueTask-friendly ergonomics, the success path stays allocation-light.

AppError

AppError is a transport-agnostic record describing what kind of failure occurred, not how a particular protocol should report it:

public sealed record AppError {
    public required ErrorKind Kind { get; init; }
    public required string Message { get; init; }
    public object? Data { get; init; }
}

Error kinds

ErrorKindMeaning
ValidationInvalid input or constraint violation.
NotFoundThe requested resource does not exist.
ConflictThe operation conflicts with existing state (duplicate, concurrent modification).
UnauthorizedThe caller is not authenticated (missing or invalid credentials). Maps to HTTP 401.
ForbiddenThe caller is authenticated but not authorized to perform this operation.
BusinessRuleA domain business rule was violated.
InternalAn unexpected internal error occurred.

Factory methods

Each kind has a factory; validation supports an optional structured payload:

AppError.Validation("Name is required.");
AppError.Validation("Invalid command.", new[] { "Name is required.", "Email is invalid." });
AppError.Validation("Invalid command.", new Dictionary<string, string[]> {   // field-keyed: renders as
    ["name"] = ["Name is required."],                                        // RFC 7807 `errors` over HTTP
});
AppError.NotFound($"Client {id} was not found.");
AppError.Conflict("Invoice number is already reserved.");
AppError.Unauthorized("Authentication required.");
AppError.Forbidden("You cannot modify this resource.");
AppError.BusinessRule("Credit limit exceeded.", data: limitInfo);
AppError.Internal("Failed to render the document.");

AppError.InternalError is a shared instance for the generic internal-failure case.

Mapping to a transport

Kind is the seam between application semantics and protocol codes. Each host owns the mapping, so different deployments can translate the same domain failure differently:

// The framework default: AppError → JSON-RPC error code (Elarion.AppErrorMapper)
var code = error.Kind switch {
    ErrorKind.Validation   => -32602,        // spec "Invalid params"
    ErrorKind.NotFound     => -32001,        // server range
    ErrorKind.Conflict     => -32002,        // server range
    ErrorKind.Forbidden    => -32003,        // server range
    ErrorKind.BusinessRule => -32004,        // server range
    ErrorKind.Unauthorized => -32005,        // server range
    _                      => -32603,        // spec "Internal error"
};

This keeps handlers free of transport details: the same AppError.NotFound(...) becomes a JSON-RPC error in one host and an HTTP 404 in another. For the complete, authoritative tables — both the JSON-RPC codes and the HTTP status mapping — see the Result & error model reference.

Results in pipelines

Decorators can inspect a Result<T> through the non-generic IResultLike interface — for example a transaction decorator that commits on success and rolls back on failure:

if (response is IResultLike { IsSuccess: true }) {
    await transaction.CommitAsync(ct);
} else {
    await transaction.RollbackAsync(ct);
}

See Decorator pipelines for the full pattern, including how a validation decorator turns failures into Result<T>.Failure(...).

On this page