Error Handling
View SourceAshTypescript provides a comprehensive error handling system that transforms Ash framework errors into TypeScript-friendly JSON responses. Errors are returned with structured information that can be easily consumed by TypeScript clients.
Error Response Format
All errors from RPC actions are returned in a standardized format:
export type AshRpcError = {
/** Machine-readable error type (e.g., "invalid_changes", "not_found") */
type: string;
/** Full error message (may contain template variables like %{key}) */
message: string;
/** Concise version of the message */
shortMessage: string;
/** Variables to interpolate into the message template */
vars: Record<string, any>;
/** List of affected field names (for field-level errors) */
fields: string[];
/** Path to the error location in the data structure */
path: string[];
/** Optional map with extra details (e.g., suggestions, hints) */
details?: Record<string, any>;
}Client-Side Variable Interpolation
Unlike server-side rendering, AshTypescript returns error messages as templates with separate variables. This allows clients to handle localization and formatting according to their needs:
// Server returns:
{
type: "required",
message: "Field %{field} is required",
vars: { field: "email" },
fields: ["email"]
}
// Client can interpolate:
function interpolateMessage(error: AshRpcError): string {
let message = error.message;
if (error.vars) {
Object.entries(error.vars).forEach(([key, value]) => {
message = message.replace(`%{${key}}`, String(value));
});
}
return message;
}Error Types
AshTypescript implements protocol-based error handling for common Ash error types:
not_found- Resource or record not foundrequired- Required field missinginvalid_attribute- Invalid attribute valueinvalid_argument- Invalid action argumentforbidden- Authorization failureforbidden_field- Field-level authorization failureinvalid_changes- Invalid changesetinvalid_query- Invalid query parametersinvalid_page- Invalid pagination parametersinvalid_keyset- Invalid keyset for paginationinvalid_primary_key- Invalid primary key valueunknown_field- Unknown or inaccessible fieldunknown_error- Unexpected error
Configuring Error Handlers
Domain-Level Error Handler
Configure a custom error handler for all resources in a domain:
defmodule MyApp.Domain do
use Ash.Domain,
extensions: [AshTypescript.Rpc]
rpc do
error_handler {MyApp.RpcErrorHandler, :handle_error, []}
end
endResource-Level Error Handler
Configure error handling for specific resources:
defmodule MyApp.Resource do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshTypescript.Resource]
rpc do
error_handler {MyApp.ResourceErrorHandler, :handle_error, []}
end
endWhen both domain and resource error handlers are defined, they are applied in sequence:
- Resource error handler (if defined)
- Domain error handler (if defined)
- Default error handler
Writing Custom Error Handlers
Error handlers receive the error and context, allowing for custom transformations:
defmodule MyApp.RpcErrorHandler do
def handle_error(error, context) do
# Context includes:
# - domain: The domain module
# - resource: The resource module (if applicable)
# - action: The action being performed
# - actor: The current actor/user
case error.type do
"forbidden" ->
# Customize forbidden errors
%{error | message: "Access denied to this resource"}
"not_found" ->
# Add custom details for not found errors
%{error | details: Map.put(error.details || %{}, :support_url, "https://example.com/help")}
_ ->
# Pass through other errors unchanged
error
end
end
endAction-Specific Error Handling
You can customize errors based on the specific action that triggered them:
defmodule MyApp.ResourceErrorHandler do
def handle_error(error, %{action: action} = context) do
case action.name do
:create ->
# Special handling for create actions
customize_create_error(error)
:update ->
# Special handling for update actions
customize_update_error(error)
_ ->
# Default handling
error
end
end
defp customize_create_error(%{type: "required"} = error) do
%{error | message: "This field is required when creating a new record"}
end
defp customize_create_error(error), do: error
defp customize_update_error(error), do: error
endCustom Error Types
To add support for custom Ash errors, implement the AshTypescript.Rpc.Error protocol:
defmodule MyApp.CustomError do
use Splode.Error, fields: [:field, :reason], class: :invalid
def message(error) do
"Custom validation failed for #{error.field}: #{error.reason}"
end
end
defimpl AshTypescript.Rpc.Error, for: MyApp.CustomError do
def to_error(error) do
%{
message: "Field %{field} failed validation: %{reason}",
short_message: "Validation failed",
type: "custom_validation_error",
vars: %{
field: error.field,
reason: error.reason
},
fields: [error.field],
path: []
}
end
endField Path Tracking
Errors include fields and path arrays that track the location of errors in data structures:
// Error in nested relationship field
{
type: "unknown_field",
message: "Unknown field 'user.invalid_field'",
fields: ["invalid_field"],
path: ["user"]
}
// Error in array element
{
type: "invalid_attribute",
message: "Invalid value at position %{index}",
vars: { index: 2 },
path: ["items", 2, "quantity"]
}Handling Multiple Errors
When multiple errors occur, they are returned as an array in the errors field:
interface RpcErrorResponse {
success: false;
errors: AshRpcError[];
}
// Client handling
async function handleRpcCall(response: any) {
if (!response.success) {
response.errors.forEach((error: AshRpcError) => {
console.error(`${error.type}: ${interpolateMessage(error)}`);
// Handle specific error types
if (error.type === "forbidden") {
redirectToLogin();
} else if (error.type === "validation_error") {
highlightFields(error.fields);
}
});
}
}TypeScript Integration
The generated TypeScript client includes full type definitions for error handling:
// Using generated RPC functions
import { createTodo } from './generated';
try {
const result = await createTodo({
title: "New Todo",
userId: "123"
});
if (result.success) {
console.log("Created:", result.data);
} else {
// TypeScript knows result.errors is AshRpcError[]
result.errors.forEach(error => {
if (error.type === "required") {
console.error(`Missing required field: ${error.fields?.[0]}`);
}
});
}
} catch (e) {
// Network or other errors
console.error("Request failed:", e);
}Best Practices
Let the client handle interpolation: Return message templates and variables separately for better localization support.
Use specific error types: Choose the most specific error type that matches the condition.
Include field information: Always populate the
fieldsarray for field-specific errors.Provide actionable messages: Error messages should guide users on how to fix the issue.
Track error paths: Use the
pathfield to indicate where in nested structures errors occurred.Add debugging context: Use the
detailsfield to include additional debugging information (but be careful not to expose sensitive data).Handle errors gracefully in TypeScript: Always check the
successfield before accessingdatain responses.
Differences from GraphQL Error Handling
Unlike AshGraphql which can interpolate variables server-side, AshTypescript intentionally returns templates and variables separately. This design choice provides:
- Better support for client-side localization
- Flexibility in message formatting
- Ability to use different messages for the same error type based on client context
- Reduced server-side processing
The error structure is also flattened compared to GraphQL's nested error format, making it easier to work with in TypeScript applications.