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.
Choose a standalone result
Section titled “Choose a standalone result”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 data | void, Task, or ValueTask |
| Give the caller a value | that value, or Task<T> / ValueTask<T> |
| Succeed with data or reject validation | Result<TResponse, ValidationResult> (possibly asynchronous) |
| Give the caller several pieces of data | one ordinary response DTO containing those values |
| Combine a response with server-side work | a 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;}// An exception implementing ValidationFailure carries validation feedback out of// the handler; its own message and stack trace are never exposed to the caller.public static final class AuthorAlreadyRegistered extends RuntimeException implements ValidationFailure { public AuthorAlreadyRegistered() { super("Duplicate author name"); }
@Override public List<ValidationResult> getValidationResults() { return List.of(ValidationResult.error( "An author with that name is already registered.", List.of("name"))); }}
public AuthorId handle(@FromServices AuthorRepository authors) { if (authors.existsByName(name)) throw new AuthorAlreadyRegistered();
authors.save(new Author(id, name)); return id;}// An exception implementing ValidationFailure carries validation feedback out of// the handler; its own message and stack trace are never exposed to the caller.class AuthorAlreadyRegistered : RuntimeException("Duplicate author name"), ValidationFailure { override val validationResults: List<ValidationResult> = listOf( ValidationResult.error("An author with that name is already registered.", listOf("name")) )}
suspend fun handle(authors: AuthorRepository): AuthorId { if (authors.existsByName(name)) throw AuthorAlreadyRegistered()
authors.save(Author(id, name)) return id}@inject(AuthorRepository)async handle(authors: AuthorRepository): Promise<AuthorId | Outcome<never>> { if (await authors.existsByName(this.name)) { return rejected(validation('An author with that name is already registered.', ['name'])); }
await authors.save({ id: this.id, name: this.name }); return this.id;}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.
Read it on the client
Section titled “Read it on the client”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.
Optional: event results with Chronicle
Section titled “Optional: event results with Chronicle”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 intention | Return shape and prerequisite |
|---|---|
| Append a registered event | AuthorRegistered 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 append | Result<AuthorRegistered, ValidationResult> |
| Append several events | an 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.
See also
Section titled “See also”- Commands — supported handlers and pipeline behavior.
- Validate a command — choose the right rejection point.
- Test a command — assert the actual write, not just a successful result.