Skip to content

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.

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.

Use the status properties to distinguish these outcomes:

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)
}

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 a CommandResult payload 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
}

Indicates whether the command payload passed validation rules.

  • true: All validation rules passed
  • false: One or more validation rules failed (check validationResults for 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}`);
});
}

Indicates whether any exceptions occurred during command execution.

  • true: One or more exceptions were thrown
  • false: 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.');
}

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 = true

Key principle: If isSuccess is false, check the other properties to determine why it failed.

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.

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 error
  • message: A developer-facing message. Only meant to be shown to a user when reason is rule
  • members: Array of property names that failed validation
  • state: Additional context, set by whoever authored the rule (FluentValidation’s WithState)
  • reason: What composed the result — see below
  • reasonDetail: Which specific thing within reason produced the result — the violated constraint for constraintViolation, or ReadChanged / ReadIncomplete for read-protection concurrencyViolation results. undefined when 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:

reasonWhat happenedWhat to do
ruleA rule you authored rejected the input. The default.Show message — it is your copy
concurrencyViolationOptional Chronicle integration: the event source moved onRe-read and offer a deliberate retry
constraintViolationOptional Chronicle integration: an event-store constraint rejected the appendMap the constraint name to your own copy
validatorFailedA validator threw; nothing the author wrote survivesShow your own generic copy, and check the server log
dependencyUnavailableA dependency needed to evaluate the command was unavailableDo not imply the input passed or failed its rules
malformedRequestThe request body or a bound value could not be read; C# supplies a normalized member when availableCorrect the request; rules were not reached. Ordinary parser details stay private; explicit-offset refusals have a showable message.
frameworkFailureThe framework could not fulfill a command contract or complete its effectsUse your own copy; this framework-composed message is not meant for end users

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.

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.

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.

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.

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) {
/* ... */
}
}

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.');
}
}

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,
});
}

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.