<!--
  Error Handling
  From the Iris book, chapter 9, covering Iris 14.0.0 on Go 1.27.
  Import path: github.com/kataras/iris/v14
  Canonical page: https://iris-go.com/docs/error-handling/
-->
# Chapter 9: Error Handling

Error handling is where Iris differs most visibly from a bare `net/http`
application. Instead of scattering `ctx.StatusCode` and ad-hoc JSON bodies
across handlers, the `rest/errors` package gives you canonical error codes,
a single wire format, and one central place where Go errors are translated
into HTTP responses. Your domain packages return plain sentinel errors; your
handlers forward them; the framework classifies, logs, and renders them.
This chapter covers the whole system: the wire format, the code catalog, the
direct senders, the central error map, field validation, private errors,
driver-specific resolvers, error-returning handlers, and the HTML and
RFC 7807 renderers.

## Background

### Errors Are Values, Not Exceptions

Go has no exceptions and no try/catch. A function that can fail returns
an `error` (an ordinary value) as its last result, and the caller
checks it with `if err != nil`. Nothing unwinds the stack, and an ignored
error is simply gone. Two standard-library idioms shape everything in
this chapter. A sentinel error is a package-level value created once with
`errors.New` and compared by identity: `store.ErrNotFound` is *the*
not-found error of that package. Wrapping adds context while keeping the
original recognizable: `fmt.Errorf("shelf 42: %w", err)` produces a new
error that still contains the sentinel, and `errors.Is` walks the chain
of wrapped errors to find it (`errors.As` does the same for typed
errors). That is why the central map matches an error no matter how many
layers of context were added on the way up.

Go reserves `panic` for a different category: programmer bugs and
unrecoverable states, not expected failures. A well-behaved server
recovers from panics so one broken request cannot crash the process.
Iris does this for you and routes the panic through the same error
pipeline as any other failure.

### Why APIs Need One Error Format

The consumer of your API is a program, not a person: browser JavaScript,
a mobile app, another service. When a request fails, that program has to
decide what to do: show a message under the right form field, send the
user to the login page, retry in a few seconds, or give up. It can only
decide mechanically if every endpoint reports failure the same way: one
JSON shape, with a machine-readable code, documented once. Without that
contract every client accumulates per-endpoint special cases, and every
error you add breaks someone. The wire format in this chapter is that
contract, and the problem that disappears from your handlers is
deciding, at each call site, what an error response should look like.

### Client Mistakes (4xx) versus Server Failures (5xx)

HTTP divides failure status codes into two families, and the split
carries real meaning (RFC 9110). A 4xx status says the *request* is
wrong: the input failed validation (400), the caller is not signed in
(401) or not allowed (403), the resource does not exist (404). The
client must change something; repeating the same request will fail the
same way. A 5xx status says the *server* failed: a bug, a crashed
dependency, an overloaded database. The client did nothing wrong and may
retry later. Everything downstream keys off this split (monitoring
should page a human for 5xx spikes but not for 404 noise, clients retry
a 503 but never a 400), which is why the code catalog below forces every
error into one family or the other.

### Why Leaking Internals Is a Security Problem

Raw error text is written for the developer who owns the code, and it
shows: SQL fragments, table and column names, file paths, internal
hostnames, driver versions. Sent to a client, each of those is free
reconnaissance for an attacker mapping your system, the class of flaw
security guides call information disclosure. The safe pattern, applied
throughout this chapter, is asymmetry: the client receives a stable,
generic message plus a unique error `id`, while the full error text goes
to the server-side logs where that `id` lets you find it. The `Private`,
`ErrPrivate` and `LogErr` tools exist to make this asymmetry the path of
least resistance.

## The Philosophy: One Place for Errors

The design has three rules:

1. **The domain defines sentinel errors.** A service or repository returns
   `store.ErrNotFound`, `store.ErrShelfLocked`. These are plain
   `errors.New` values that know nothing about HTTP.
2. **The application maps them once.** A single `errors.NewOptions()` value,
   handed to the builder's `Errors` step, associates each sentinel with a
   canonical code. `errors.Is` does the matching, so wrapping with
   `fmt.Errorf("...: %w", err)` anywhere in the call chain never breaks it.
3. **Handlers just forward.** `ctx.OK`, `errors.HandleError` and friends send
   any non-nil error through the same pipeline. A handler never decides
   status codes for domain failures.

```go
// store/store.go
package store

import "errors"

var (
    ErrNotFound    = errors.New("store: not found")
    ErrDuplicate   = errors.New("store: already exists")
    ErrShelfLocked = errors.New("store: shelf locked")
)
```

```go
// main.go (excerpt)
app := iris.NewBuilder().
    Prefix("/api").
    AllowOrigin("none").
    Compression(true).
    LogRequests(true).
    Health(true, "production", "admin").
    Errors(errors.NewOptions().
        MapErrors(errors.NotFound, store.ErrNotFound).
        MapErrors(errors.AlreadyExists, store.ErrDuplicate).
        MapErrors(errors.PermissionDenied, store.ErrShelfLocked)).
    Services( /* repositories, services ... */ ).
    RouterMiddlewares().
    Middlewares().
    API("/books", NewBooksAPI).
    Build()
```

(The chain order is compiler-enforced and every step appears exactly once;
`Errors` sits between `Health` and `Services`. See
[Chapter 3](https://iris-go.com/docs/iris-newbuilder/).)

```go
// api/books.go (excerpt)
func (api *BooksAPI) get(ctx iris.Context) {
    book, err := api.svc.Book(ctx, ctx.Params().GetString("id"))
    ctx.OK(book, err)
}
```

If the service returns `store.ErrNotFound` (even wrapped three levels
deep), the client receives a `404 NOT_FOUND` wire error. Unmapped errors
become a generic `500 INTERNAL`: the message sent to the client is a
constant, the real error is logged server-side. Clients never see your SQL.

## The Wire Format

Every error response shares one JSON structure, the "HTTP wire error"
(`errors.Error`):

```json
{
    "id": "3e9c1b6e-8a3e-4a3c-9d7a-2f2c1a6b5e4d",
    "http_error_code": {
        "canonical_name": "NOT_FOUND",
        "status": 404
    },
    "message": "store: not found",
    "detail": "no book with id '42'",
    "validation": [
        {"field": "title", "value": "", "reason": "must not be empty"}
    ],
    "data": {"hint": "list books at /api/books"}
}
```

- `id`: unique identifier per occurrence, a UUID by default. Use it to
  correlate a client report with server logs.
- `http_error_code`: the canonical name and its HTTP status.
- `message`: short, human-readable summary.
- `detail` (optional): explanation specific to this occurrence.
- `validation` (optional): array of field-level failures.
- `data` (optional): structured extra data, any JSON value.

(A UUID, or universally unique identifier, is a 128-bit random value with
a standard 36-character text form. Collisions are so improbable that one
can be generated anywhere, with no coordination, which makes it a good
per-occurrence tag.)

The struct also carries optional `type` and `instance` fields for RFC 7807
problem details, covered [below](#html-and-rfc-7807-rendering).

## Canonical Error Codes

An `errors.ErrorCodeName` couples a stable, machine-readable name with an
HTTP status code. The most used ones:

| Constant                      | Canonical name           | Status |
| ----------------------------- | ------------------------ | ------ |
| `errors.InvalidArgument`      | `INVALID_ARGUMENT`       | 400    |
| `errors.FailedPrecondition`   | `FAILED_PRECONDITION`    | 400    |
| `errors.OutOfRange`           | `OUT_OF_RANGE`           | 400    |
| `errors.Unauthenticated`      | `UNAUTHENTICATED`        | 401    |
| `errors.PaymentRequired`      | `PAYMENT_REQUIRED`       | 402    |
| `errors.PermissionDenied`     | `PERMISSION_DENIED`      | 403    |
| `errors.NotFound`             | `NOT_FOUND`              | 404    |
| `errors.MethodNotAllowed`     | `METHOD_NOT_ALLOWED`     | 405    |
| `errors.AlreadyExists`        | `ALREADY_EXISTS`         | 409    |
| `errors.Aborted`              | `ABORTED`                | 409    |
| `errors.TooLarge`             | `TOO_LARGE`              | 413    |
| `errors.UnsupportedMediaType` | `UNSUPPORTED_MEDIA_TYPE` | 415    |
| `errors.UnprocessableEntity`  | `UNPROCESSABLE_ENTITY`   | 422    |
| `errors.ResourceExhausted`    | `RESOURCE_EXHAUSTED`     | 429    |
| `errors.Internal`             | `INTERNAL`               | 500    |
| `errors.DataLoss`             | `DATA_LOSS`              | 500    |
| `errors.Unknown`              | `UNKNOWN`                | 500    |
| `errors.Unimplemented`        | `UNIMPLEMENTED`          | 501    |
| `errors.BadGateway`           | `BAD_GATEWAY`            | 502    |
| `errors.Unavailable`          | `UNAVAILABLE`            | 503    |
| `errors.DeadlineExceeded`     | `DEADLINE_EXCEEDED`      | 504    |

The package registers a constant for effectively every HTTP failure status
(406–511), plus a few Iris-specific ones such as `errors.InvalidToken` (498)
and `errors.TokenRequired` (499). Register your own during initialization
when the built-in vocabulary is not enough:

```go
import (
    "net/http"

    "github.com/kataras/iris/v14/rest/errors"
)

// UserBlocked renders as 403 with canonical name USER_BLOCKED.
var UserBlocked = errors.Register("USER_BLOCKED", http.StatusForbidden)
```

Registration is not thread-safe by design; do it in `init` or early in
`main`, before the server starts.

## Sending Errors Directly

Every code is also a sender. Each method below writes the wire error and
stops the request, except `Wrap`, which only tags an error for later
processing:

| Method           | Signature (after the code)                                        | Purpose                                  |
| ---------------- | ----------------------------------------------------------------- | ---------------------------------------- |
| `Message`        | `(ctx, message string)`                                           | Message only                             |
| `Messagef`       | `(ctx, format string, args ...any)`                               | Formatted message                        |
| `Detail`         | `(ctx, msg, detail string)`                                       | Message + detail                         |
| `Detailf`        | `(ctx, msg, detail string, args ...any)`                          | Formatted detail                         |
| `Data`           | `(ctx, msg string, data any)`                                     | Message + JSON data                      |
| `DataWithDetail` | `(ctx, msg, detail string, data any)`                             | All three                                |
| `Validation`     | `(ctx, errs ...ValidationError)`                                  | Field failures                           |
| `Err`            | `(ctx, err error)`                                                | `err.Error()` as the message             |
| `ErrPrivate`     | `(ctx, err error)`                                                | Generic message, error kept server-side  |
| `LogErr`         | `(ctx, err error)`                                                | Logs; `Internal` sends a generic message |
| `Log`            | `(ctx, format string, args ...any)`                               | Like `LogErr` with formatting            |
| `Problem`        | `(ctx, title, detail, typeURI, instanceURI string, extra ...any)` | RFC 7807                                 |
| `Wrap`           | `(err error) error`                                               | Tags an error, sends nothing             |

```go
import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/rest/errors"
)

func handler(ctx iris.Context) {
    // Message and detail.
    errors.NotFound.Detailf(ctx, "book not found",
        "no book with id %q", ctx.Params().GetString("id"))

    // Structured data alongside the message.
    errors.FailedPrecondition.Data(ctx, "insufficient funds", iris.Map{
        "current_balance":  50.00,
        "required_balance": 100.00,
    })
}
```

One sentence gets written often enough to have its own function.
`errors.NotFoundID(ctx, resource, id)` renders `NOT_FOUND` with the detail
`"<resource> <id> not found"`, which is the `Detailf` above with its
format string spelled out for the hundredth time:

```go
func getPlan(ctx iris.Context) {
    plan, err := svc.GetPlan(ctx, id)
    if errors.HandleError(ctx, err) {
        return
    }

    if plan == nil {
        errors.NotFoundID(ctx, "plan", id)
        return
    }

    ctx.OK(plan, nil)
}
```

The id is formatted with `%v`, so a string, an integer and a `uuid.UUID`
all read the way they print. Its `bool` result follows the `Context.OK`
convention, `true` meaning the handler succeeded, so it is always `false`
here; note that `HandleError` in the same package reports the opposite,
`true` when it wrote the response, which is why the example tests one and
ignores the other.

### Err, LogErr and ErrPrivate

The three error-value senders differ in what the client sees and what is
logged:

```go
// Client sees err.Error() as the message under the given code.
// Validation errors are auto-routed to Validation().
errors.InvalidArgument.Err(ctx, err)

// Logs the error. For errors.Internal the client gets a generic
// message; for any other code the error text is sent.
errors.Internal.LogErr(ctx, err)

// The client always gets the generic message
// ("server error" / "an unexpected error occurred while
// processing the request"); the real error stays server-side.
errors.Internal.ErrPrivate(ctx, err)
```

Use `Err` for errors that are safe to show, `LogErr` as the default for
server faults, and `ErrPrivate` when the error text may leak schema, paths
or credentials. `Log` and `LogErr` also skip canceled requests: when the
client has disconnected, nothing is logged or sent (package-level setting
`errors.SkipCanceled`, enabled by default).

`Err` has one more convenience: if the error value is a struct, its exported
fields are marshaled into the response's `data` member automatically, so a
typed domain error carries its context to the client without extra code.

### Wrap: tag now, send later

`Wrap` joins the code with an error so a lower layer can decide the status
without touching the context:

```go
func (s *Service) Publish(ctx context.Context, id string) error {
    n, err := s.repo.Publish(ctx, id)
    if err != nil {
        return err
    }
    if n == 0 {
        return errors.NotFound.Wrap(fmt.Errorf("book %q", id))
    }
    return nil
}
```

When this error later reaches `errors.HandleError`, the tag is detected and a
`404 NOT_FOUND` with message `book "42"` is sent. Prefer the central map for
recurring cases; reach for `Wrap` when the classification depends on local
information the map cannot see.

## The Central Error Map

`errors.NewOptions()` returns an `*errors.Options` whose error-code map is
**not** empty. The defaults (built in `rest/errors/options.go`) map:

- `database/sql.ErrNoRows` → `NotFound`
- the `github.com/kataras/jwt` token errors (`jwt.ErrExpired`,
  `jwt.ErrMissing`, `jwt.ErrTokenSignature`, `jwt.ErrBlocked` and the rest
  of the family) → `Unauthenticated`
- `view.ErrNoEngine` (from `middleware/view`) → `Internal`
- hex-decode failures (`encoding/hex: invalid byte`) → `InvalidArgument`,
  re-worded as "invalid secret format"

PostgreSQL mappings are deliberately not included; they are opt-in through
the [`pgerrors` subpackage](#database-errors-and-resolvers).

Every `Map...` call extends the map; nothing is replaced:

```go
import (
    "fmt"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/rest/errors"

    "myapp/store"
)

opts := errors.NewOptions().
    // Sentinel values, matched with errors.Is (wrapping-safe).
    MapErrors(errors.NotFound, store.ErrNotFound).
    MapErrors(errors.AlreadyExists, store.ErrDuplicate).

    // Whole types, matched with errors.As semantics.
    MapErrorTypes(errors.InvalidArgument, store.QuotaError{}).

    // A function: return nil to pass, or the error to send.
    MapErrorFunc(errors.Internal, func(err error) error {
        if store.IsRetryable(err) {
            return errors.Private(err) // hide the cause from the client
        }
        return nil
    }).

    // Context-aware variant: enrich with request detail.
    MapErrorFuncCtx(errors.NotFound,
        func(ctx iris.Context, err error) error {
            if errors.Is(err, store.ErrNotFound) {
                return fmt.Errorf("%s: %w", ctx.Path(), err)
            }
            return nil
        })
```

- `MapErrors(code, targets ...error)`: exact sentinels via `errors.Is`.
- `MapErrorTypes(code, targets ...any)`: concrete or interface types;
  pass a typed nil pointer (`(*MyError)(nil)`) for interfaces.
- `MapErrorFunc(code, func(error) error)`: transform or claim errors;
  functions for the same code run in registration order until one returns
  non-nil. The two closure bodies people write most often, `errors.As` on
  a concrete type and a substring test on the message, have their own
  calls; see the next section before reaching for this one.
- `MapErrorFuncCtx(code, func(iris.Context, error) error)`: the same, with
  access to the request context. The context is nil when the map's
  `Filter(ctx, err)` method is called directly outside a request, so
  tolerate nil if you call it yourself.

Candidates are evaluated in ascending status-code order, so more specific
client-error classifications win over catch-all 500 mappings.

### Mapping a typed error, and mapping a text

Two shapes of `MapErrorFunc` closure are common enough to have their own
call. The first finds a concrete error type and builds the response from
its fields; without a helper it is a closure declaring a variable, calling
`errors.As`, and returning `nil` on the way out.

```go
opts.MapErrorAs(errors.ResourceExhausted,
    func(e *store.QuotaError) *errors.Error {
        return &errors.Error{
            Message: "quota exceeded",
            Detail:  fmt.Sprintf("the limit is %d", e.Limit),
        }
    })
```

`errors.As` looks for the type anywhere in the error tree, so a wrapped
one matches, and the `render` function receives the value it found. A nil
result means "not mine" and the next mapping gets a turn, exactly like
`MapErrorFunc` returning nil. Passing a nil `render` classifies without
transforming, which needs the type spelled out because there is nothing
left to infer it from: `opts.MapErrorAs[*store.QuotaError](code, nil)`.

`MapErrorAs` is a generic method on both `*errors.Options` and a bare
`errors.ErrorCodeMap`, and it returns the receiver, so it chains after
`MapErrors` like every other mapping call. The type parameter is
inferred from the `render` function. When you need the same lookup
inside your own code, `errors.AsType[*store.QuotaError](err)` is the
standard library's `errors.AsType`, forwarded by `rest/errors` next to
`Is` and `As`, so a file that imports the package as `errors` needs no
second import.

The second shape sniffs a substring, which is all a driver leaves you when
its error arrives as text:

```go
opts.MapErrorContains(errors.InvalidArgument,
    "violates check constraint", "invalid value")
```

The match is case-sensitive and the message replaces the original text, so
the driver's own words never reach the client. Two caveats come with it.
Prefer a typed mapping wherever one exists, because a driver is free to
reword its text in the next release and a substring mapping fails silently
when it does. And because the replacement discards the original, log
before you map when those words are the only diagnostic you have, the way
`pgerrors` does with `errors.LogError`. An empty `substr` registers
nothing at all rather than a mapping that matches every error: a missing
constant is a mistake, and the safe reading of it is "map nothing".

### Wiring the options

With the builder, pass the options to the `Errors` step (its position in
the chain is compiler-enforced):

```go
iris.NewBuilder().
    // ... Prefix, AllowOrigin, Compression, LogRequests, Health ...
    Errors(opts).
    // ... Services, RouterMiddlewares, Middlewares, API, Build ...
```

With a manual `iris.New()` application, register the options' middleware
yourself, before the routes it should cover:

```go
app := iris.New()
app.UseRouter(opts.Handler())
```

`opts.Handler()` stores the options on each request's context; every sender
and `HandleError` call then finds the map, logger, collector and renderer
you configured. `UseRouter` makes it cover 404/405 paths too; plain
`app.Use(opts.Handler())` covers matched routes only (see
[Chapter 4](https://iris-go.com/docs/routing-controllers/) for the middleware families).

## Forwarding Errors from Handlers

`errors.HandleError(ctx, err) bool`, from `rest/errors`, is the
front door of the pipeline. It returns false for a nil error, otherwise it
classifies the error, writes the response and returns true:

```go
import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/rest/errors"
)

func (api *BooksAPI) create(ctx iris.Context) {
    input, ok := ctx.BindJSON[CreateBookInput]()
    if !ok {
        return // error response already written
    }

    book, err := api.svc.Create(ctx, input)
    if errors.HandleError(ctx, err) {
        return
    }

    ctx.Created(book, nil)
}
```

`ctx.Created(book, err)` would fold those three middle lines into itself,
which is why handlers built on the response helpers rarely name
`HandleError`. Call it directly for an error that arrives outside a
helper's reach: from a guard, from a middleware, from a stream already in
flight.

`ctx.HandleError(err)` is the same thing as a context method, for a step
that has an error but nothing to render on success:

```go
func (api *PlansAPI) deactivate(ctx iris.Context) {
    if ctx.HandleError(api.svc.DeactivatePlans(ctx, id)) {
        return
    }

    ctx.NoContent(true, nil)
}
```

It is the error half that `OK`, `Created`, `NoContent`, `List`, `Count`
and `Paginated` each run before rendering their success body, on its own.
A handler whose only use of `rest/errors` was that one call no longer
imports the package.

When the failure of one step means something specific at that call site
and nothing in general, name the code instead of mapping it.
`errors.<Code>.Handle(ctx, err)` renders the error as that code and
reports whether it did:

```go
func (api *AuthAPI) changeEmail(ctx iris.Context) {
    input, ok := ctx.BindJSON[ChangeEmailInput]()
    if !ok {
        return
    }

    if ctx.HandleError(api.identity.ChangeEmail(ctx, input)) {
        return
    }

    // A third party being down is Unavailable here, whatever the
    // central map would have made of it.
    if errors.Unavailable.Handle(ctx, api.users.UpdateEmail(ctx, input.Old, input.New)) {
        return
    }

    ctx.NoContent(true, nil)
}
```

`Handle` is `Err` plus the answer to "did it fire", and a nil error writes
nothing and reports false. `Err` keeps the signature it has, because
`func(*iris.Context, error)` is what `LogErrorFunc` and
`cors.HandleErrorFunc` accept and
`cors.New().HandleErrorFunc(errors.FailedPrecondition.Err)` passes it
straight to them.

The classification order inside `HandleError` is a waterfall: the first
test that recognizes the error writes the response, so a specific rule
always beats a general one.

\[Diagram: errors.HandleError walks ten tests in order and the first match writes the response, so a more specific rule always beats a more general one and an unrecognised error ends at a logged 500.]

The family has three siblings:

- `errors.HandleRequestError(ctx, err)`: identical, except that it treats a
  failure to decode the request body as the client's fault. It differs at
  step 6 only: a JSON decoding error renders `400 INVALID_ARGUMENT`
  ("unable to parse request body") rather than a 500. An error that no step
  recognizes still ends at `500 INTERNAL` either way. Use it when
  processing user input, where a decode failure is almost certainly the
  client's.
- `errors.HandleErrorf(ctx, err, format, args...)`: wraps the error with
  call-site detail (using `%w`, so mappings still apply), then handles it:

```go
// Renders the mapped 404 with message "shelf 42: store: not found".
if err := api.svc.Rename(ctx, id, input.Title); err != nil {
    errors.HandleErrorf(ctx, err, "shelf %s", id)
    return
}
```

- `errors.HandleRequestErrorf`: the same enrichment with the 400 fallback.

The response helpers all call `HandleError` internally: `ctx.OK`,
`ctx.Created`, `ctx.NoContent`, `ctx.Count` and `ctx.Paginated`
([Chapter 6](https://iris-go.com/docs/response-handling/)). That is why a typical handler body
is one line: `ctx.OK(result, err)`.

## Field Validation

`errors.ValidationError` is a small interface (`error` plus
`GetReason() string`), and `errors.ValidationErrors` is a slice of them.
The `rest/errors/validation` subpackage ships fluent, generic builders:

| Constructor                      | Chainable rules                                                                             |
| -------------------------------- | ------------------------------------------------------------------------------------------- |
| `validation.String(name, value)` | `NotEmpty()`, `Length(min, max)`, `Email()`, `Fullname()`, `Password(level)`, `Phone(kind)` |
| `validation.Number(name, value)` | `Positive()`, `Negative()`, `Zero()`, `NonZero()`, `InRange(min, max)`                      |
| `validation.Slice(name, value)`  | `NotEmpty()`, `Length(min, max)`                                                            |
| `validation.Field(name, value)`  | `Func(fns ...func(T) string)`, custom rules                                                 |

Rules accumulate reasons; `validation.Join` filters out the fields that
passed and returns nil when everything is valid. The idiomatic v14 place
for this is a `Validate` method on the request type; `ctx.BindJSON[T]()`
calls it automatically ([Chapter 5](https://iris-go.com/docs/reading-requests/)):

```go
import (
    "github.com/kataras/iris/v14/rest/errors/validation"
)

type CreateBookInput struct {
    Title string `json:"title"`
    Pages int    `json:"pages"`
    Email string `json:"email"`
}

func (in *CreateBookInput) Validate() error {
    return validation.Join(
        validation.String("title", in.Title).NotEmpty().Length(1, 200),
        validation.Number("pages", in.Pages).Positive(),
        validation.String("email", in.Email).Email(),
    )
}
```

A failed validation renders with a fixed message and detail and one entry
per failing field. This is the real wire format:

```json
{
    "http_error_code": {
        "canonical_name": "INVALID_ARGUMENT",
        "status": 400
    },
    "message": "validation failure",
    "detail": "fields were invalid",
    "validation": [
        {"field": "title", "value": "",
         "reason": "must not be empty, must be between 1 and 200 characters"},
        {"field": "pages", "value": 0, "reason": "must be positive"},
        {"field": "email", "value": "bad",
         "reason": "must be a valid email address"}
    ]
}
```

Outside `Validate` methods, send field errors explicitly, either through
`Err` (validation errors are auto-routed) or `Validation` directly:

```go
name := validation.String("name", input.Name).NotEmpty().Length(3, 50)
email := validation.String("email", input.Email).Email()

if err := validation.Join(name, email); err != nil {
    errors.InvalidArgument.Err(ctx, err)
    return
}
```

Custom rules are plain functions from the value to a reason string (empty
means valid):

```go
username := validation.Field("username", input.Username).Func(
    validation.NotEmpty,
    func(value string) string {
        if strings.Contains(value, " ") {
            return "must not contain spaces"
        }
        return ""
    },
)

if err := username.Get(); err != nil { // Get() returns nil when valid
    errors.InvalidArgument.Err(ctx, err)
    return
}
```

Any type of your own that implements `Error() string` and
`GetReason() string` participates in the same machinery, useful when
adapting a third-party validator's output.

## Private Errors

Two complementary tools keep sensitive error text away from clients:

- **`errors.Private(err)`** marks an error at its *creation site*. Wherever
  it later surfaces in the pipeline, the client receives the generic
  message while the original text is preserved for server-side logging.
  Use it in service layers that cannot know how the error will be rendered,
  or inside `MapErrorFunc` transformations.
- **`code.ErrPrivate(ctx, err)`** decides at the *response site*: generic
  message now, real error kept server-side. That holds for every error
  type: a `*errors.Error` or a validation error gets the generic message
  here too.

```go
// Service layer: mark once, forget about it.
return errors.Private(
    fmt.Errorf("query failed on host %s: %w", host, err))

// Handler: forwards as usual; the marker is detected automatically.
errors.HandleError(ctx, err)
```

The scrubbed response is always the same shape:

```json
{
    "http_error_code": {"canonical_name": "INTERNAL", "status": 500},
    "message": "server error",
    "detail": "an unexpected error occurred while processing the request"
}
```

That is the shape `HandleError` and the senders produce.
`errors.Handler` answers a private stored error by the status it
carries, and it does so before any mapping is asked. Below 500 the
client reads the status text and no detail: a private error stored with
`ctx.StopWithError(401, iris.PrivateError(err))` reads
`"message":"Unauthorized"`, not a server fault. At 500 and above it
reads the generic "server error". The status the handler set stays, even
when your map would have turned the error into a 404. The `jwt`
middleware stops every bad token this way, so its 401 reads
`Unauthorized` with or without your error options, and a client cannot
learn from the body why its token was refused. The private text still
reaches the collector.

Note the asymmetry worth memorizing: `errors.Internal.LogErr(ctx, err)` is
generic to the client, but `errors.Internal.Err(ctx, err)` sends the error
text. When in doubt on a 500 path, use `LogErr`.

## Database Errors and Resolvers

`sql.ErrNoRows` is mapped out of the box. PostgreSQL mappings for the
`github.com/kataras/pg` driver are opt-in, so the driver stays out of your
dependency graph unless you use it:

```go
import _ "github.com/kataras/iris/v14/rest/errors/pgerrors"
```

(A blank import, the `_` alias, compiles a package purely for its side
effects: its `init` functions run, registering the resolvers below, and
no identifier enters your namespace.)

The blank import restores four behaviors inside `HandleError`:

- `pg.ErrNoRows` → `404 NOT_FOUND` ("resource not found")
- duplicate-key violations → `409 ALREADY_EXISTS` with a generic client
  detail; the raw driver error (which leaks table, column and constraint
  names) is logged server-side only
- foreign-key violations (SQLSTATE `23503`) → `400 INVALID_ARGUMENT`
  ("invalid reference"), naming the constraint when the driver reports one so
  the client can tell which reference was wrong. A row that points at
  something which is not there is bad input, not a server fault, and
  without this branch it fell through to a 500. The driver's own message
  and detail, which name the table, the column and the offending value,
  are logged and never forwarded
- SQLSTATE-tagged input errors ("invalid input syntax", "operator does not
  exist") → `400 INVALID_ARGUMENT` with the SQLSTATE noise trimmed

(SQLSTATE is the SQL standard's five-character error code, attached to
every PostgreSQL error: `23505` is a unique-key violation, `22P02` an
invalid text representation. The resolver reads it to tell client-caused
input errors apart from genuine server faults.)

The subpackage is built on `errors.RegisterErrorResolver`, a public
extension point you can use for any driver or SDK. Resolvers run inside
`HandleError` after the per-route options map and before the built-in
classification; the first one that reports true stops the chain:

```go
import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/rest/errors"
)

func init() {
    errors.RegisterErrorResolver(
        func(ctx iris.Context, err error) bool {
            var terr *mydriver.TimeoutError
            if !errors.As(err, &terr) {
                return false // not ours, keep going
            }

            errors.Unavailable.Message(ctx,
                "storage temporarily unavailable")
            return true
        })
}
```

Register resolvers at init time or before serving, like error codes.

## Handlers That Return Errors

Writing `if err != nil { errors.HandleError(ctx, err); return }` in every
handler gets repetitive. The `errors.Router` wrapper accepts handlers of
type `errors.Func` (`func(ctx iris.Context) error`) and pipes any
returned error through `HandleError` automatically. A controller opts in
by taking `*errors.Router` in its `Routing` method; the framework's
controller resolver recognizes it (plain routing functions
`func(r *errors.Router)` work too):

```go
import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/rest/errors"

    "myapp/reports"
)

type ReportsAPI struct {
    svc *reports.Service
}

func NewReportsAPI(svc *reports.Service) *ReportsAPI {
    return &ReportsAPI{svc: svc}
}

func (api *ReportsAPI) Routing(r *errors.Router) {
    r.Get("/{year:int}", api.byYear)
}

func (api *ReportsAPI) byYear(ctx iris.Context) error {
    report, err := api.svc.ByYear(ctx, ctx.Params().GetInt("year"))
    if err != nil {
        return err // classified by the central map
    }

    return ctx.JSON(report)
}
```

Register it like any controller: `API("/reports", NewReportsAPI)` in the
builder, or `app.Router("/reports", NewReportsAPI(svc))` manually. The
wrapper exposes the full routing surface
(`Get`, `Post`, `Put`, `Delete`, `Patch`, `Any`, `Subdomain`, `Use`,
`UseRouter`) with error-returning signatures. Middleware follows the same
rule: a nil return continues the chain, a non-nil error is handled and
stops it:

```go
r.Use(func(ctx iris.Context) error {
    if api.svc.Rebuilding(ctx) {
        return errors.Unavailable.Wrap(
            errors.New("reports are being rebuilt"))
    }
    return nil
})
```

Write these for the rules only your code knows. Authentication is not one
of them: a machine caller presenting a shared secret is `apikey.New` from
`middleware/apikey` ([Chapter 12](https://iris-go.com/docs/built-in-middleware/#api-keys)),
which renders the 401 through this same pipeline.

For a single handler on a plain router, `errors.FuncHandler(fn)` converts
an `errors.Func` to a standard handler, and `errors.FuncMiddleware(fn)`
does the same with `ctx.Next()` semantics.

## HTML and RFC 7807 Rendering

JSON is the default rendering, with two built-in alternatives.

### HTML error pages

When the request or the already-set response `Content-Type` is `text/html`,
wire errors render as HTML pages automatically. The `errors.HTML` renderer
looks for a view template named after the status code (`errors/404.html`,
`errors/500.html`) using the view engine registered through
`middleware/view` ([Chapter 6](https://iris-go.com/docs/response-handling/)); if no engine or
template exists, a minimal inline page with the status, canonical name and
message is served. To force HTML for every error regardless of content
type:

```go
opts := errors.NewOptions().SetRenderer(errors.HTML)
```

Inside the template, the wire error is the root value: `{{.Message}}`,
`{{.Detail}}`, `{{.ErrorCode.Status}}`, `{{.ID}}`.

### RFC 7807 problem details

RFC 7807, "Problem Details for HTTP APIs", is the IETF's standard answer
to the format question this chapter opened with: a small, extensible
JSON vocabulary (`type`, `title`, `status`, `detail`, `instance`)
served under its own media type so generic clients and tooling can
recognize an error response without knowing your API. Reach for it when
you must interoperate with consumers that expect the standard shape.

Each code has a `Problem` sender that produces an RFC 7807 "Problem
Details" response with `Content-Type: application/problem+json`:

```go
errors.InvalidArgument.Problem(ctx,
    "Validation Failed",                              // title
    "The email field contains an invalid address",    // detail
    "https://example.com/problems/validation-failed", // type URI
    "/users/create")                                  // instance URI
```

```json
{
    "id": "3e9c1b6e-8a3e-4a3c-9d7a-2f2c1a6b5e4d",
    "status": 400,
    "title": "Validation Failed",
    "type": "https://example.com/problems/validation-failed",
    "detail": "The email field contains an invalid address",
    "instance": "/users/create"
}
```

Trailing `extra ...any` values become extension members: a single
object-shaped value is merged into the problem at the top level, anything
else (or multiple values) renders under a `data` key, nils are filtered.
The problem format
is used whenever `type`, `instance` and a title are present and no custom
renderer is registered; to render *every* error as problem+json, set the
package's renderer:

```go
opts := errors.NewOptions().SetRenderer(errors.Problem)
```

`SetRenderer` accepts any `func(ctx iris.Context, err *errors.Error)`, so
XML, content negotiation or a bespoke envelope are equally possible.

## Logging, Collection and Error IDs

The `Options` value carries three more hooks, all chainable:

```go
import (
    "log/slog"
    "uuid"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/rest/errors"
)

opts := errors.NewOptions().
    // Called for logged errors (LogErr, Log, private errors,
    // unmapped 500s). Default: the application logger.
    SetLogger(func(ctx iris.Context, err error) {
        slog.Error("http error",
            "error", err.Error(),
            "path", ctx.Path(),
            "method", ctx.Method(),
        )
    }).
    // Called with the complete wire error before it is sent
    // (and before private scrubbing), synchronously — offload
    // slow work to a goroutine.
    SetCollector(func(ctx iris.Context, err *errors.Error) {
        slog.LogAttrs(ctx, slog.LevelWarn,
            "wire error", err.Attrs()...)
    }).
    // Error ID generation. Default: a UUID per error.
    SetIDGenerator(func(ctx iris.Context) string {
        if id := ctx.Header("X-Request-Id"); id != "" {
            return id
        }
        return uuid.New().String()
    })
```

- `SetLogger(func(ctx iris.Context, err error))` overrides logging for
  routes covered by these options; the package-level `errors.LogError`
  variable is the global fallback.
- `SetCollector` feeds monitoring: the collector observes the full error
  (including details that private scrubbing removes from the client copy).
  `(*errors.Error).Attrs()` returns ready-made `slog` attributes if you
  log wire errors structurally.
- `SetIDGenerator` / `SetID` control the `id` field. Static IDs via
  `SetID` are for tests; production should keep unique IDs so a client
  screenshot can be matched to a log line.

### Reporting without rendering

Every method so far ends in a render, which is fine while the response is
still unwritten. A streamed response is not: after its first byte the
status code is spent, and writing an error body into the middle of an
event stream corrupts it. Skipping the error system instead is worse,
because the logger and the collector are the only things that carry a
failure anywhere, and a stream that fails silently is a stream nobody
knows failed.

`errors.Report(ctx, err)` runs the whole pipeline and returns the `*Error`
without writing it:

```go
reported := errors.Report(ctx, err) // nil when there is nothing to report.
```

It resolves the error through the route's error-code map, honours
`SkipCanceled`, calls the logger with the real error, generates the id,
redacts a private or internal error down to a generic message, and calls
the collector. What comes back is the client-safe copy, and it is the
caller's to put wherever the transport allows.
`errors.ErrorCodeName.Report(ctx, err)` is the same thing under a code you
already know, for a failure that has been classified elsewhere.

This is what the `sse` package writes its error frame with, so a streaming
handler gets the same treatment as every other one without asking for it.
[Chapter 6](https://iris-go.com/docs/response-handling/) covers the frame, and
[Chapter 21](https://iris-go.com/docs/ai-backends/) covers what a relayed upstream failure looks
like once it has been through here.

## Router-Level Defaults

Builder-built applications get the error system's defaults wired without
any code:

- **Bare failure statuses**: a status of 400 or above that reaches the
  client with nothing written answers the JSON envelope, with the status
  text as the message. It does not matter whether the handler stopped
  the chain: `ctx.StopWithStatus(403)`, `ctx.NotFound()`, the 401 of the
  `jwt` and `basicauth` middleware and a file the file server does not
  have all answer it, under the canonical name and status text of their
  own status (`PERMISSION_DENIED` and "Forbidden" for a 403). The 413 of
  the `bodylimit` middleware is the one whose message is not the status
  text: `TOO_LARGE` with "request body too large".
- **Unknown paths and wrong methods**: an unknown path answers
  `404 NOT_FOUND`. A method the path does not serve answers
  `405 METHOD_NOT_ALLOWED`, with the `Allow` header intact, when
  `Options.Router.FireMethodNotAllowed` is on; it is off by default, and
  the answer is then the 404 one.
- **Typed path parameters**: a request that fails a macro type check
  (`/users/{id:uuid}` receiving `abc`) renders `400 INVALID_ARGUMENT`
  with message "invalid path parameter", the parse failure as detail and
  the offending parameter as data.
- **Panics**: the recovery middleware routes panics through
  `errors.Internal.LogErr`: a logged, generic 500 wire error.
- **Rate limiting**: when the `rate` middleware trips, the client gets
  `429 RESOURCE_EXHAUSTED` with "you may try again in 2s" as detail.
- **Response write failures**: context-level errors are forwarded to
  `HandleError`.

The first two come from `errors.Handler`, which the builder registers as
the last of its built-in router middleware, after compression, and as
the 404 and the 405 handler. Your own `app.HandleNoRoute` or
`app.HandleNoMethod`, registered from a `Configurator` given to
`NewBuilder` or on the value `iris.Default()` returns, still wins: the
builder fills those two slots only when they are empty at `Build`.

What the client reads for an error stored on the context follows the
rules of the rest of this chapter, with one rule ahead of them: a
private error is never rendered. Private means stored through
`iris.PrivateError` or `ctx.SetErrPrivate`, or carrying the mark
anywhere in its chain, which is what `errors.Private` builds and what a
private error wrapped again with `%w` still is. Such an error is
answered before any mapping is asked, with the status the handler set:
below 500 the status text is the message, and at 500 and above the
client reads the generic "server error" while the error goes to the
logger. Any other stored error that your map or a resolver recognises
renders through its mapping; a malformed JSON body stored next to a 400,
for example, answers `400 INVALID_ARGUMENT` with "unable to parse
request body". Otherwise, below 500 the error's text is the message. At
500 and above a stored `*errors.Error` is sent as it is, because that
type exists to be sent to clients, and anything else goes to the logger
while the client reads "server error". A database that is down never has
its address read by a client because somebody forgot to mark the error.
`ctx.StopWithPlainError` and `ctx.SetErr` store an error and write
nothing, so the rules above decide the body. `ctx.StopWithError` with an
error that is not private writes the error's text itself, so that
response has a body and `errors.Handler` leaves it alone. With a private
error, whether `iris.PrivateError(err)`, `errors.Private(err)` or a
rewrap of either, it sets the status, writes nothing and stores the
error as private, so the private rule answers it.

A response that already has a body is left alone, and so is one that
declares a `Content-Length`, one whose status line is already on the
wire, or one whose connection was hijacked. The declared length matters
for standard handlers mounted with `iris.FromStd`: one that sets
`Content-Length: 0` and calls `w.WriteHeader(401)`, which is what
`httputil.ReverseProxy` relays for an upstream's empty 401, keeps its
empty answer, while one that sets a failure status and neither writes,
flushes nor declares a length gets the envelope.

Two failures are answered ahead of `errors.Handler` and keep their bare
status: a rejection from a handler that a `Configurator` registered with
`UseRouter`, which runs before the builder's own middleware, and the 415
the compression middleware sends for a request body that is not what its
`Content-Encoding` says.

Manual applications can adopt the same defaults. Where you register
`errors.Handler` decides what it covers, and `UseRouter` alone is not
enough for 404 and 405: it runs before the router, and by the time the
chain comes back the router's default handler has already written
`Not Found`. Register it as the no-route and no-method handler as well:

```go
func init() { errors.ApplyPatches() }

app := iris.New()
app.UseRouter(opts.Handler())      // your central map
app.UseRouter(errors.Handler)      // bare failure statuses
app.HandleNoRoute(errors.Handler)  // 404 as a wire error
app.HandleNoMethod(errors.Handler) // 405 (FireMethodNotAllowed)
```

`app.Use(errors.Handler)` on the root application covers the routes and
the router's 404 and 405 in one line, but then a `HandleNoRoute` or
`HandleNoMethod` registered after it never runs, because in those two
chains `errors.Handler` answers and does not call the next handler. For
a custom non-JSON 404 page on an `iris.New()` application, skip
`errors.Handler` there and register your own handlers with
`app.HandleNoRoute` / `app.HandleNoMethod` ([Chapter
4](https://iris-go.com/docs/routing-controllers/)). On a builder application, register them
from a `Configurator` passed to `iris.NewBuilder`, as above.

## Summary

- Domain packages return sentinel errors; one `errors.NewOptions()` map,
  wired through the builder's `Errors` step (or
  `app.UseRouter(opts.Handler())`), translates them; handlers forward with
  `ctx.OK` / `errors.HandleError`.
- Every error renders as one wire format: `id`, `http_error_code`
  (canonical name + status), `message`, optional `detail`, `validation`
  and `data`.
- Codes are senders: `Message`, `Detail`, `Data`, `Err`, `LogErr`,
  `ErrPrivate`, `Validation`, `Problem`, and `Wrap` for deferred handling.
  On 500 paths prefer `LogErr` or `ErrPrivate`; never leak internals.
- `MapErrors` matches sentinels, `MapErrorTypes` matches types,
  `MapErrorFunc`/`MapErrorFuncCtx` transform; the default map already
  covers `sql.ErrNoRows`, JWT errors and more. `pgerrors` adds PostgreSQL
  via a blank import; `RegisterErrorResolver` is the extension point.
- Validation builders (`validation.String/Number/Slice/Field` +
  `validation.Join`) produce the standard `validation failure` response;
  `ctx.BindJSON[T]()` runs your `Validate` method automatically.
- `errors.Router` lets handlers and middleware return errors instead of
  writing responses; `errors.HTML` and `errors.Problem` provide HTML pages
  and RFC 7807 output; `SetLogger`, `SetCollector` and `SetIDGenerator`
  hook logging, monitoring and correlation IDs.

## Further Reading

- [The Go Blog: Error handling and Go](https://go.dev/blog/error-handling-and-go):
  the foundational piece on errors as values and the `error` interface.
- [The Go Blog: Working with Errors in Go 1.13](https://go.dev/blog/go1.13-errors).
  Wrapping with `%w`, `errors.Is` and `errors.As`: the exact mechanics
  the central map relies on.
- [RFC 9110 — HTTP Semantics: Status Codes](https://www.rfc-editor.org/rfc/rfc9110#name-status-codes):
  the authoritative definition of every 4xx and 5xx status.
- [MDN: HTTP response status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status):
  the same catalog in reference-card form, with usage notes.
- [RFC 7807 — Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc7807):
  the problem+json format behind the `Problem` sender (superseded by
  RFC 9457, which keeps the same wire format).
- [OWASP Error Handling Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html):
  why clients get generic messages and logs get the details.
