Skip to content

Return a result or an error

Goal: explicitly write through a database or service, then return a value the caller needs — or a typed rejection. Arc.Core does not append events just because a returned object has an event-like name.

The return shapes in this table are C# signatures. The JVM and TypeScript fragments below show their corresponding handler contracts.

You want to…Return
Complete without response datavoid, Task, or ValueTask
Give the caller a valuethat value, or Task<T> / ValueTask<T>
Succeed with data or reject validationResult<TResponse, ValidationResult> (possibly asynchronous)
Give the caller several pieces of dataone ordinary response DTO containing those values
Combine a response with server-side worka tuple containing one ordinary response and operations or other server-handled values

The following handler replacement uses the MongoDB tutorial’s setup and domain types. On C#, add using Cratis.Monads; and using Cratis.Arc.Validation; to the command file:

public async Task<Result<AuthorId, ValidationResult>> Handle(IMongoCollection<Author> authors)
{
if (await authors.Find(author => author.Name == Name).AnyAsync())
{
return ValidationResult.Error("An author with that name is already registered.", [nameof(Name)]);
}
await authors.InsertOneAsync(new Author(Id, Name));
return Id;
}

View C# snippet source on GitHub

The backends reject differently, and it is worth being explicit about it. C# declares the alternative in the signature with Result<TResponse, ValidationResult>. Arc.Kotlin has no Result<,> type and no handler return shape that carries both a client response type and a rejection, because the generated proxy and OpenAPI metadata need exactly one client response leaf. The JVM way is to throw an application exception implementing io.cratis.arc.validation.ValidationFailure: the command pipeline converts it into the same validation feedback a validator would produce, without leaking the exception message or stack trace. An ordinary exception still becomes an exception result. TypeScript’s handle() returns rejected(validation(message, members)) for a typed failure, or the response value on success; it does not use C#‘s Result<,> signature.

The insert is the persistent effect; the AuthorId is response data. The pre-check gives a friendly failure but is not atomic. Keep the database unique index from validation and explicitly translate its duplicate-write failure if you need friendly errors under races. Other storage exceptions remain exception results; do not disguise them as ordinary validation.

For rules that can reject before work begins, prefer a backend command validator; use a typed handler failure when the write/decision itself discovers the failure. Provide() can also short-circuit with typed validation.

After regenerating the proxy, execute() returns a CommandResult (a response-bearing result for this signature). Check isSuccess before using response; otherwise inspect validationResults, isAuthorized, and exception information. The command-result contract documents these independently — success is not synonymous with absence of validation errors.

On C#, only after configuring Arc + Chronicle do registered-event response handlers append events. These are integration signatures, not replacements for a standalone write:

Integrated intentionReturn shape and prerequisite
Append a registered eventAuthorRegistered Handle(); Chronicle resolves the source from the command
Select the source in the response(AuthorId, AuthorRegistered) only when AuthorId derives from Chronicle EventSourceId / EventSourceId<T>
Return caller data alongside an event(result, event); unhandled values remain response data
Reject or appendResult<AuthorRegistered, ValidationResult>
Append several eventsan enumerable of registered events; use EventForEventSourceId for explicit cross-stream targeting

The standalone tutorial’s AuthorId : ConceptAs<Guid> is not an event-source identifier. An ordinary Guid in a response remains ordinary caller data; do not reinterpret it as an append target. See Chronicle response identifiers for explicit-source contracts.