CommandResult
When a command is executed, it returns a CommandResult<TResponse> that provides comprehensive information about the execution outcome. The result includes success/failure status, validation errors, authorization status, exceptions, and optional response data.
Structure
Section titled “Structure”The CommandResult interface provides the following properties:
interface ICommandResult<TResponse = object> { readonly correlationId: Guid; readonly isSuccess: boolean; readonly isAuthorized: boolean; readonly isValid: boolean; readonly hasExceptions: boolean; readonly validationResults: ValidationResult[]; readonly exceptionMessages: string[]; readonly authorizationFailureReason: string; readonly exceptionStackTrace: string; readonly response?: TResponse;}authorizationFailureReason carries the reason a command was not authorized when the server supplies one; it is empty otherwise.
Status Properties
Section titled “Status Properties”Use the status properties to distinguish these outcomes:
isSuccess
Section titled “isSuccess”Indicates whether the command executed successfully overall. This is the primary indicator of whether the operation completed as intended.
true: The command executed without any issues (authorized, valid, and no exceptions)false: The command failed for one or more reasons (could be authorization, validation, or exceptions)
Use this when:
- You want to know if the overall operation succeeded
- You need a single boolean check for success/failure
- You’re implementing simple success/failure UI feedback
const result = await command.execute();if (result.isSuccess) { // Show success message} else { // Handle failure (check other properties for details)}isAuthorized
Section titled “isAuthorized”Indicates whether Arc’s command pipeline authorized this command.
true: The result reports no authorization failure. A client-validation short circuit can produce this without running server authorization; it is not an access grant.false: The generated command endpoint returned an authorization failure, typically HTTP 403. An upstream authentication challenge can return HTTP 401 before aCommandResultpayload is produced.
Use this when:
- You need to distinguish authorization failures from other errors
- You want to show specific “access denied” messages
- You need to redirect users to login or show permission requests
const result = await command.execute();if (!result.isAuthorized) { // Show "Access Denied" message // Redirect to login or request permissions}isValid
Section titled “isValid”Indicates whether the command payload passed validation rules.
true: All validation rules passedfalse: One or more validation rules failed (checkvalidationResultsfor details)
Use this when:
- You need to display validation errors to users
- You want to highlight invalid form fields
- You’re implementing client-side validation feedback
const result = await command.execute();if (!result.isValid) { // Display validation errors from result.validationResults result.validationResults.forEach((error) => { console.log(`${error.members.join('.')}: ${error.message}`); });}hasExceptions
Section titled “hasExceptions”Indicates whether any exceptions occurred during command execution.
true: One or more exceptions were thrownfalse: No exceptions occurred
Use this when:
- You need to catch unexpected server errors
- You want to log errors for debugging
- You need to show generic error messages for system failures
const result = await command.execute();if (result.hasExceptions) { // Log exception details console.error('Exceptions:', result.exceptionMessages); console.error('Stack trace:', result.exceptionStackTrace);
// Show user-friendly error message showErrorMessage('An unexpected error occurred. Please try again.');}Understanding the Relationship
Section titled “Understanding the Relationship”The status properties work together to provide a complete picture:
// Scenario 1: Complete success// isSuccess = true, isAuthorized = true, isValid = true, hasExceptions = false
// Scenario 2: Authorization failure// isSuccess = false, isAuthorized = false, isValid = true, hasExceptions = false
// Scenario 3: Validation failure// isSuccess = false, isAuthorized = true, isValid = false, hasExceptions = false
// Scenario 4: Exception occurred// isSuccess = false, isAuthorized = true, isValid = true, hasExceptions = true
// Scenario 5: Multiple failures (validation + exception)// isSuccess = false, isAuthorized = true, isValid = false, hasExceptions = trueKey principle: If isSuccess is false, check the other properties to determine why it failed.
Accessing Data
Section titled “Accessing Data”Response Data
Section titled “Response Data”When the command returns data, access it through the response property:
interface CreateAccountResponse { accountId: string; accountNumber: string;}
const result = await createAccountCommand.execute();if (result.isSuccess && result.response) { const { accountId, accountNumber } = result.response; console.log(`Account created: ${accountNumber}`);}The response is typed according to the command’s generic parameter, providing full type safety.
Generated proxies preserve collection responses. A C# command returning IEnumerable<ProjectSummary> generates Command<IYourCommand, ProjectSummary[]> and uses ProjectSummary[] as the response type in its React useCommand hook. Calling execute() returns CommandResult<ProjectSummary[]>, so you can iterate or map result.response after checking that it is present, without casting it. An IEnumerable<object> response becomes Record<string, unknown>[]; a scalar ProjectSummary response remains ProjectSummary. Commands without response data omit the response generic and retain the default object type.
Validation Results
Section titled “Validation Results”Access detailed validation errors through the validationResults array:
const result = await command.execute();if (!result.isValid) { result.validationResults.forEach((error) => { console.log(`Severity: ${error.severity}`); console.log(`Message: ${error.message}`); console.log(`Members: ${error.members.join('.')}`); console.log(`State: ${JSON.stringify(error.state)}`); console.log(`Reason: ${error.reason}`); });}Each ValidationResult contains:
severity: The severity level of the validation errormessage: A developer-facing message. Only meant to be shown to a user whenreasonisrulemembers: Array of property names that failed validationstate: Additional context, set by whoever authored the rule (FluentValidation’sWithState)reason: What composed the result — see belowreasonDetail: Which specific thing withinreasonproduced the result — the violated constraint forconstraintViolation, orReadChanged/ReadIncompletefor read-protectionconcurrencyViolationresults.undefinedwhen the reason carries no finer identity
Telling one kind of rejection from another
Section titled “Telling one kind of rejection from another”A rejection your own rules produced and one the framework composed on your behalf arrive in the same array, in the same shape. reason is what separates them, so you never have to match the message text:
reason | What happened | What to do |
|---|---|---|
rule | A rule you authored rejected the input. The default. | Show message — it is your copy |
concurrencyViolation | Optional Chronicle integration: the event source moved on | Re-read and offer a deliberate retry |
constraintViolation | Optional Chronicle integration: an event-store constraint rejected the append | Map the constraint name to your own copy |
validatorFailed | A validator threw; nothing the author wrote survives | Show your own generic copy, and check the server log |
dependencyUnavailable | A dependency needed to evaluate the command was unavailable | Do not imply the input passed or failed its rules |
malformedRequest | The request body or a bound value could not be read; C# supplies a normalized member when available | Correct the request; rules were not reached. Ordinary parser details stay private; explicit-offset refusals have a showable message. |
frameworkFailure | The framework could not fulfill a command contract or complete its effects | Use your own copy; this framework-composed message is not meant for end users |
Optional Chronicle rejection handling
Section titled “Optional Chronicle rejection handling”The next two fragments apply only when using the Chronicle command integration. Standalone Arc has no event-store dependency. Commands can call ordinary services and return DTOs or Guid responses without any event-sourcing semantics. reloadCurrentState, offerResubmit, and setFieldError below represent application-owned actions, not Arc APIs.
import { ValidationResultReason } from '@cratis/arc/validation';
const result = await command.execute();
const conflict = result.validationResults.find( (_) => _.reason === ValidationResultReason.ConcurrencyViolation,);
if (conflict) { if (conflict.reasonDetail === 'ReadChanged' || conflict.reasonDetail === 'ReadIncomplete') { // Keep the user's input. Nothing from this command was written. await reloadCurrentState(); return offerResubmit(); }
// Other concurrency violations also need fresh state and a deliberate retry. await reloadCurrentState(); return offerResubmit();}ReadChanged is retryable: a matching event arrived after the decision read. ReadIncomplete is also retryable: the read could not finish after the fold’s retries. Keep the draft, re-read, and let the user review fresh state before resubmitting; do not clear the form or label the input invalid. Arc and Chronicle do not re-run the command. A ReadChanged result’s state includes { readModel, key }. When Chronicle reports the event that changed the read, the state also has conflictingEventTypeId, conflictingEventType and conflictingEventSequenceNumber. There is no TypeScript constant for these detail strings.
Telling one constraint from another
Section titled “Telling one constraint from another”reason says a constraint on the event store rejected the append; reasonDetail says which one. Two uniqueness constraints on the same command produce the same reason, so branching on it alone cannot pick the copy that belongs to each — and the message is developer text you should not be matching on either. Branch on the constraint name:
const result = await command.execute();
const rejectedBy = (constraint: string) => result.validationResults.some( (_) => _.reason === ValidationResultReason.ConstraintViolation && _.reasonDetail === constraint, );
if (rejectedBy('UniqueOrganizationNumber')) { return setFieldError( 'organizationNumber', 'That organization number is already registered.', );}The value is the constraint’s own name as Chronicle reports it on the violation — the same name a backend spec asserts with ShouldHaveConstraintViolationFor.
Exception Details
Section titled “Exception Details”Access exception information when hasExceptions is true:
const result = await command.execute();if (result.hasExceptions) { // Array of exception messages result.exceptionMessages.forEach((msg) => { console.error('Exception:', msg); });
// Stack trace, when the server exposes exception detail console.error('Stack trace:', result.exceptionStackTrace);}For model-bound commands, exception detail is exposed only when ArcOptions.ExposeExceptionDetails is true, which is the default in Development only. Elsewhere the server replaces the messages with a generic message, leaves exceptionStackTrace empty, and logs the full detail server-side; use correlationId to find it in the logs. Controller-based commands currently return exception messages and stack traces without that redaction, so do not rely on it for controller endpoints. See the command result envelope.
Chaining Callbacks
Section titled “Chaining Callbacks”CommandResult supports a fluent API for handling different outcomes:
const result = await command.execute();
result .onSuccess((response) => { console.log('Success!', response); }) .onFailed((commandResult) => { console.log('Failed:', commandResult); }) .onUnauthorized(() => { console.log('Not authorized'); }) .onValidationFailure((validationResults) => { console.log('Validation errors:', validationResults); }) .onException((messages, stackTrace) => { console.error('Exception:', messages, stackTrace); });Each callback method returns the CommandResult instance, allowing you to chain multiple handlers.
Best Practices
Section titled “Best Practices”1. Check isSuccess First
Section titled “1. Check isSuccess First”Always start by checking isSuccess for the overall outcome:
const result = await command.execute();if (result.isSuccess) { // Handle success} else { // Check specific failure reasons if (!result.isAuthorized) { /* ... */ } if (!result.isValid) { /* ... */ } if (result.hasExceptions) { /* ... */ }}2. Provide Specific Error Messages
Section titled “2. Provide Specific Error Messages”Use the specific status properties to give users meaningful feedback:
const result = await command.execute();if (!result.isSuccess) { if (!result.isAuthorized) { showMessage('You do not have permission to perform this action.'); } else if (!result.isValid) { showValidationErrors(result.validationResults); } else if (result.hasExceptions) { showMessage('An unexpected error occurred. Please contact support.'); }}3. Log Exceptions for Debugging
Section titled “3. Log Exceptions for Debugging”Always log exception details when hasExceptions is true:
if (result.hasExceptions) { logger.error('Command execution failed', { messages: result.exceptionMessages, stackTrace: result.exceptionStackTrace, correlationId: result.correlationId, });}4. Use Correlation ID for Tracking
Section titled “4. Use Correlation ID for Tracking”The correlationId helps track command executions across the system:
console.log(`Command executed with correlation ID: ${result.correlationId}`);This is especially useful for debugging and support scenarios.
Related Topics
Section titled “Related Topics”- Commands - Core command concepts and usage
- React Commands - Using commands in React components
- Validation - Understanding validation rules