Elarion
Core Concepts

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).
ForbiddenThe caller is 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.NotFound($"Client {id} was not found.");
AppError.Conflict("Invoice number is already reserved.");
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:

// Example: AppError → JSON-RPC error code
var code = error.Kind switch {
    ErrorKind.Validation  => -32602,        // Invalid params
    ErrorKind.NotFound    => -32004,
    ErrorKind.Conflict    => -32009,
    ErrorKind.Forbidden   => -32003,
    ErrorKind.BusinessRule=> -32010,
    _                     => -32603,        // 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.

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