Error Handling in AshCommanded

View Source

This document explains the error handling system in AshCommanded, which provides standardized, structured error reports and consistent error handling across the entire framework.

Overview

AshCommanded provides a standardized approach to error handling through the AshCommanded.Commanded.Error module. This system ensures that:

  1. Errors have a consistent structure across the framework
  2. Error messages are user-friendly and informative
  3. Developers can easily identify the source and context of errors
  4. Errors can be properly formatted for display to users
  5. Errors from different sources (Ash, Commanded) are normalized to a common format

Error Structure

All errors in AshCommanded are represented using the AshCommanded.Commanded.Error struct, which has the following fields:

FieldTypeDescription
typeatomThe category of error (e.g., :validation_error, :command_error)
messagestringA human-readable error message
pathlistPath to the error in a data structure (for nested errors)
fieldatomThe specific field that caused the error (if applicable)
valueanyThe value that caused the error (if applicable)
contextmapAdditional contextual information about the error

Error Types

The following error types are used throughout AshCommanded:

TypeDescriptionCommon Uses
:validation_errorError validating command parametersParameter validation failures
:transformation_errorError transforming command parametersType casting failures, computation errors
:command_errorError related to command structure or processingMissing required fields, invalid command structure
:aggregate_errorError in aggregate processingCommand execution failures in aggregates
:dispatch_errorError dispatching a commandRouter errors, event store errors
:action_errorError executing an Ash actionResource action failures
:projection_errorError applying a projectionEvent handling errors in projectors

Creating Errors

You can create errors using the constructor functions in the AshCommanded.Commanded.Error module:

# General constructor
Error.new(:validation_error, "Value must be positive", field: :age, value: -5)

# Type-specific constructors
Error.validation_error("Value must be positive", field: :age, value: -5)
Error.transformation_error("Failed to cast to integer", field: :age, value: "abc")
Error.command_error("Missing required field", field: :id)
Error.aggregate_error("Aggregate not found", context: %{aggregate_id: "123"})
Error.action_error("Failed to execute action", context: %{action: :create_user})

Error Normalization

AshCommanded automatically normalizes errors from different sources into the standard format. This is handled through the normalize_error/1 and normalize_errors/1 functions:

# Convert an Ash error to AshCommanded format
ash_error = %Ash.Error.Invalid{errors: [%Ash.Error.Changes.InvalidAttribute{field: :name, message: "can't be blank"}]}
normalized_error = Error.normalize_error(ash_error)

# Convert a Commanded error to AshCommanded format
commanded_error = %Commanded.Aggregates.ExecutionError{message: "Failed to execute command"}
normalized_error = Error.normalize_error(commanded_error)

# Normalize a list of mixed errors
errors = [ash_error, commanded_error, "Simple string error"]
normalized_errors = Error.normalize_errors(errors)

Error Formatting

To display errors in a human-readable format, use the format/1 function:

error = Error.validation_error("Value must be positive", field: :age, value: -5)
formatted = Error.format(error)
# => "Validation error: Value must be positive (field: age, value: -5)"

Error Handling in Components

Parameter Validation

The parameter validator uses standardized error handling:

params = %{name: "John", email: "invalid-email", age: 15}
validations = [
  {:validate, :name, [min_length: 5]},
  {:validate, :email, [format: ~r/@.*\./]},
  {:validate, :age, [min: 18]}
]

case AshCommanded.Commanded.ParameterValidator.validate_params(params, validations) do
  :ok ->
    # Proceed with valid parameters
    
  {:error, errors} ->
    # Handle validation errors
    formatted_errors = Enum.map(errors, &Error.format/1)
    IO.puts("Validation failed: #{Enum.join(formatted_errors, ", ")}")
end

Command Action Mapper

The command action mapper handles errors throughout the command execution process:

result = AshCommanded.Commanded.CommandActionMapper.map_to_action(
  command,
  MyApp.User,
  :create_user,
  transforms: transforms,
  validations: validations
)

case result do
  {:ok, record} ->
    # Command executed successfully
    
  {:error, errors} when is_list(errors) ->
    # Multiple errors occurred
    formatted_errors = Enum.map(errors, &Error.format/1)
    IO.puts("Command failed: #{Enum.join(formatted_errors, ", ")}")
    
  {:error, error} ->
    # A single error occurred
    IO.puts("Command failed: #{Error.format(error)}")
end

Aggregate Error Handling

The generated aggregate modules include standardized error handling:

def execute(%__MODULE__{} = aggregate, %MyApp.Commands.RegisterUser{} = command) do
  try do
    # Command handling logic...
  rescue
    e in _ ->
      {:error, Error.aggregate_error("Error executing command: #{Exception.message(e)}", 
        context: %{command: command.__struct__, error: inspect(e)})}
  end
end

Extracting Errors from Results

To extract errors from command results, use the errors_from_result/1 function:

result = AshCommanded.Commands.RegisterUser.execute(command)

errors = Error.errors_from_result(result)
if Enum.empty?(errors) do
  IO.puts("Command succeeded!")
else
  formatted_errors = Enum.map(errors, &Error.format/1)
  IO.puts("Command failed: #{Enum.join(formatted_errors, ", ")}")
end

Best Practices

  1. Always use standardized errors: Use the Error module for all error creation rather than returning raw atoms or strings.

  2. Include contextual information: When creating errors, include relevant context such as field names, values, and additional contextual data.

  3. Handle errors gracefully: Design your code to handle errors properly, using pattern matching to extract error information.

  4. Use proper error types: Choose the appropriate error type for each situation to help with debugging and error handling.

  5. Format errors for display: When displaying errors to users, use the format/1 function to create readable error messages.

Integration with Ash Framework

AshCommanded automatically converts Ash errors to the standardized format, making it easier to integrate with existing Ash resources:

case AshCommanded.Commanded.CommandActionMapper.map_to_action(command, resource, action_name) do
  {:ok, result} ->
    # Success
    
  {:error, errors} ->
    # Both Ash errors and AshCommanded errors will be properly formatted
    formatted_errors = Enum.map(Error.normalize_errors(errors), &Error.format/1)
end