documentation/topics/errors.md

<!--
SPDX-FileCopyrightText: 2020 Zach Daniel

SPDX-License-Identifier: MIT
-->

# Errors

AshJsonApi converts Ash errors into [JSON:API error objects](https://jsonapi.org/format/#errors). This topic covers how that conversion works, how to customize it, and the available configuration options.

## Error Format

Every error response follows the JSON:API error object format:

```json
{
  "errors": [
    {
      "id": "a1b2c3d4-...",
      "status": "422",
      "code": "invalid_attribute",
      "title": "InvalidAttribute",
      "detail": "must be present",
      "source": {
        "pointer": "/data/attributes/name"
      }
    }
  ]
}
```

## The `AshJsonApi.ToJsonApiError` Protocol

AshJsonApi uses the `AshJsonApi.ToJsonApiError` protocol to convert Ash exceptions into `AshJsonApi.Error` structs. Built-in implementations are provided for common Ash errors such as `Ash.Error.Changes.InvalidChanges`, `Ash.Error.Query.NotFound`, `Ash.Error.Forbidden.Policy`, and others.

If your application raises a custom Ash exception and you want it to produce a specific JSON:API error, implement the protocol:

```elixir
defimpl AshJsonApi.ToJsonApiError, for: MyApp.Errors.PaymentRequired do
  def to_json_api_error(error) do
    %AshJsonApi.Error{
      id: Ash.UUID.generate(),
      status_code: 402,
      code: "payment_required",
      title: "PaymentRequired",
      detail: error.message,
      meta: %{}
    }
  end
end
```

The `AshJsonApi.Error` struct has the following fields:

| Field | Description |
|---|---|
| `id` | Unique identifier for this error occurrence |
| `status_code` | HTTP status code (integer) |
| `code` | Machine-readable error code string |
| `title` | Human-readable error title |
| `detail` | Human-readable explanation specific to this occurrence |
| `source_pointer` | JSON Pointer to the source of the error (e.g. `/data/attributes/name`) |
| `source_parameter` | Query parameter that caused the error |
| `meta` | Arbitrary metadata map |
| `about` | Link to further information about this error |
| `log_level` | Log level for this error (default: `:debug`) |
| `internal_description` | Internal description used for logging, not sent to clients |

## Transforming Errors with `error_handler`

The `error_handler` domain option lets you intercept and transform any `AshJsonApi.Error` struct before it is sent to the client. This is useful for sanitizing error messages, adding metadata, translating error text, or applying any other cross-cutting transformation.

Configure it in your domain as an MFA:

```elixir
defmodule MyApp.Domain do
  use Ash.Domain, extensions: [AshJsonApi.Domain]

  json_api do
    error_handler {MyApp.JsonApiErrorHandler, :handle_error, []}
  end
end
```

The handler receives the `AshJsonApi.Error` struct and a context map, and must return a modified `AshJsonApi.Error` struct:

```elixir
defmodule MyApp.JsonApiErrorHandler do
  def handle_error(error, _context) do
    # Sanitize internal details from 500 errors
    if error.status_code >= 500 do
      %{error | detail: "An internal error occurred. Please try again later."}
    else
      error
    end
  end
end
```

The context map contains:

| Key | Description |
|---|---|
| `:domain` | The domain module handling the request |
| `:resource` | The resource module associated with the request (may be `nil`) |

### Example: Translating Error Messages

```elixir
defmodule MyApp.JsonApiErrorHandler do
  def handle_error(error, _context) do
    %{error | detail: MyApp.Gettext.translate_error(error.code, error.detail)}
  end
end
```

### Example: Adding Custom Metadata

```elixir
defmodule MyApp.JsonApiErrorHandler do
  def handle_error(error, %{domain: domain}) do
    %{error | meta: Map.put(error.meta || %{}, :api_version, "v2")}
  end
end
```

### Example: Context-Specific Handling

```elixir
defmodule MyApp.JsonApiErrorHandler do
  def handle_error(error, %{resource: resource}) do
    case resource do
      MyApp.PaymentResource ->
        %{error | detail: MyApp.Payments.format_error(error)}

      _ ->
        error
    end
  end
end
```

## Configuration Options

### `show_raised_errors?`

By default, if an error is *raised* (i.e. an unexpected exception, not a structured Ash error), AshJsonApi returns a generic error message with only a UUID for reference. This prevents leaking internal implementation details.

Set `show_raised_errors? true` to include the full exception in the response — useful during development:

```elixir
json_api do
  show_raised_errors? true
end
```

### `log_errors?`

Controls whether errors are logged. Defaults to `true`.

```elixir
json_api do
  log_errors? false
end
```

### Policy Breakdown Details

By default, authorization failures return a generic "forbidden" message. To include a breakdown of which policies failed (useful for debugging), set this in your application config:

```elixir
# config/dev.exs
config :ash_json_api, :policies, show_policy_breakdowns?: true
```

> **Warning:** Do not enable this in production, as it may expose details about your authorization logic.