# Errors

Match typed errors, map them to stable HTTP statuses, and keep secrets out of error text.

Errors are typed values with stable codes. Handlers match the error, pick a status, and log once (see [Logging](https://zenta-dev.github.io/zever/guides/operate/logging/)). There is no config block or accessor: errors come from services you resolved, and `config.Default()` returns the same typed values as production.

## Quick start

Match with `errors.Is`, write a stable message, and log only the unexpected branch:

```go title="handlers.go"
func showUser(w http.ResponseWriter, r *http.Request) {
    user, err := findUser(r.Context(), id)
    if errors.Is(err, db.ErrNotFound) {
        writeError(w, http.StatusNotFound, "user not found")
        return
    }
    if err != nil {
        logger.Error().Err(err).Msg("find user failed")
        writeError(w, http.StatusInternalServerError, "internal error")
        return
    }
    writeJSON(w, http.StatusOK, user)
}
```

```sh
curl -s localhost:8080/v1/users/missing -w "\n%{http_code}\n"
```

```text
{"error":"user not found"}
404
```

Log once, at the boundary. Libraries add context with `%w`; handlers choose the status. `writeJSON` and `writeError` come from [Requests and responses](https://zenta-dev.github.io/zever/guides/http/requests-responses/). A helper that returns `true` lets the caller skip the generic 500. Wrap with `%w` to keep identity, ORM failures carry `orm:` and `orm/render:` prefixes, and the DSL mirrors `apperror` codes in `ir.ErrorCode`.

`.zen` RPCs declare their error set (`errors: { already_exists }`) so clients and OpenAPI know which codes to expect.

## Map errors to statuses

| Error | Status | Envelope |
|---|---|---|
| `db.ErrNotFound` | 404 | `{"error":"user not found"}` |
| `dialect.ErrUnsupportedByDialect` | 400 | `{"error":"query not supported by this database"}` |
| `already_exists` (RPC error set) | 409 | `{"error":"already exists"}` |
| Rate-limit deny | 429 plus `Retry-After` | `{"error":"rate limit exceeded"}` |
| Handler timeout (`middleware.Timeout`) | 504 | `{"error":"request timed out"}` |
| Handler panic (`middleware.Recover`) | 500 | `{"error":"internal error"}` |
| Anything else | 500 | `{"error":"internal error"}` |

Also in use: 400 unparsable, 422 invalid ([Validation](https://zenta-dev.github.io/zever/guides/http/validation/)).

## Handle shutdown errors

`c.Close(ctx)` returns an `errors.Join` of per-service errors. Match `container.ClosePanicError` and `container.CloseTimeoutError` (value types) with `errors.As`, then log the joined error once at exit:

```go
var pe container.ClosePanicError
if errors.As(err, &pe) { /* a service panicked during Close */ }
```

## Keep secrets out of error text

Error text names services and fields, never values. The same rule covers logs and displayed config: `zever config show` prints redacted config. Render config only through `config.RedactedServices`.

## Troubleshooting

<details>
<summary>`errors.Is(err, db.ErrNotFound)` never matches</summary>

A layer dropped the chain with `%v` or a new error. Wrap with `%w` at every layer.
</details>

<details>
<summary>A 500 body leaks internals</summary>

The handler writes `err.Error()` to the client. Write a stable slug and log the detail.
</details>

<details>
<summary>A dialect failure looks like a bug</summary>

A postgres-only feature on sqlite returns `dialect.ErrUnsupportedByDialect` by design. Match it and use a portable formulation.
</details>

<details>
<summary>The shutdown error is opaque</summary>

You print only the first error. `Close` joins all of them: log the full value, then use `errors.As` for panic and timeout cases.
</details>

## Related

- [Validation](https://zenta-dev.github.io/zever/guides/http/validation/): 400 vs 422 vs 409 at the boundary.
- [Middleware](https://zenta-dev.github.io/zever/guides/http/middleware/): Recover and timeout envelopes.
- [Logging](https://zenta-dev.github.io/zever/guides/operate/logging/): One structured line per failure.
