PouryaKit
Concepts

Operation, Result and Error

An Operation is one command or query running inside one Module; it ends in a Result, and a business failure is an Error in it.

What it is

An Operation is one command or query running inside one Module, from entry to outcome. A command changes state and a query only reads. Each is a sealed record with exactly one handler, and every handler returns a Result: a value on success, one or more Errors on failure.

An Error is a named business reason the Operation did not succeed. It has a stable code, a message and a kind, and it carries no HTTP status: the Web Capability turns its kind into one when the Result becomes a response. Result and Error lists the kinds, and Problem responses the status each answers.

A business failure is an Error, never an exception

Return an Error when the Operation cannot succeed for a reason the business knows, such as a missing product or a name already in use. Exceptions are for bugs and infrastructure. A test of a business or validation failure asserts on the Result's Errors, never on an exception.

When it matters

Every Feature has one. A command that changes something, such as renaming a product, is a Command or a Command<TResponse>. A read, such as one product or a page of them, is a Query<TResponse>. Each passes the same fixed steps, so a Feature supplies only its record, its handler and, when it needs them, a validator and an authorizer.

How it works

A Business Area declares its Errors once, as static fields:

ProductErrors.cs
public static readonly Error NameRequired = Error.Validation(
    "Product.NameRequired",
    "A product needs a name.",
    "Name"
);

public static readonly Error NotFound = Error.NotFound(
    "Product.NotFound",
    "The product does not exist."
);

// Mapped to the unique index on the name in CatalogModule: the database, not a check beforehand, refuses a duplicate.
public static readonly Error NameInUse = Error.Conflict(
    "Product.NameInUse",
    "Another product already has this name."
);

A handler returns either a value or an Error, and each converts to a Result. This one, from the Proving Sample's RenameProduct Feature, answers the Business Area's not-found Error when there is no such product, and otherwise whatever the aggregate's rule answers:

RenameProductCommandHandler.cs
internal sealed class RenameProductCommandHandler(IRepository<Product, ProductId> products)
    : CommandHandler<RenameProductCommand>
{
    public override async Task<Result> Handle(
        RenameProductCommand command,
        CancellationToken cancellationToken
    )
    {
        var product = await products.FindAsync(new ProductId(command.ProductId), cancellationToken);
        return product is null ? ProductErrors.NotFound : product.Rename(command.Name);
    }
}

Every command and query passes the same fixed pipeline before and around its handler; The pipeline lists its steps. A command's handler runs inside the Unit of Work, which commits once for the whole Operation: see Unit of Work. A failure at any step, a nested command's included, fails the Operation with its Errors.

  • Pass a failed Result's Errors on rather than ignoring them: every Error is kept.
  • Check IsSuccess or IsFailure before you read Value.
  • Check a business rule that depends on state in the aggregate, with CheckRule, and input shape in a validator.

What the Kit guarantees

GuaranteeStated in
How a value or an Error becomes a ResultResult and Error
One handler per command or queryCommands and queries
The fixed pipeline, and what a failed nested command doesThe pipeline
What a query, an authorizer or a validator may changeCode that only reads
Which HTTP status each kind of Error answersProblem responses

Failure messages

Next steps

On this page