Error Handling in AshCommanded
View SourceThis 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:
- Errors have a consistent structure across the framework
- Error messages are user-friendly and informative
- Developers can easily identify the source and context of errors
- Errors can be properly formatted for display to users
- 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:
Field | Type | Description |
---|---|---|
type | atom | The category of error (e.g., :validation_error , :command_error ) |
message | string | A human-readable error message |
path | list | Path to the error in a data structure (for nested errors) |
field | atom | The specific field that caused the error (if applicable) |
value | any | The value that caused the error (if applicable) |
context | map | Additional contextual information about the error |
Error Types
The following error types are used throughout AshCommanded:
Type | Description | Common Uses |
---|---|---|
:validation_error | Error validating command parameters | Parameter validation failures |
:transformation_error | Error transforming command parameters | Type casting failures, computation errors |
:command_error | Error related to command structure or processing | Missing required fields, invalid command structure |
:aggregate_error | Error in aggregate processing | Command execution failures in aggregates |
:dispatch_error | Error dispatching a command | Router errors, event store errors |
:action_error | Error executing an Ash action | Resource action failures |
:projection_error | Error applying a projection | Event 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
Always use standardized errors: Use the
Error
module for all error creation rather than returning raw atoms or strings.Include contextual information: When creating errors, include relevant context such as field names, values, and additional contextual data.
Handle errors gracefully: Design your code to handle errors properly, using pattern matching to extract error information.
Use proper error types: Choose the appropriate error type for each situation to help with debugging and error handling.
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