# `Decibel.SessionError`
[🔗](https://github.com/ausimian/decibel/blob/1.2.1/lib/decibel/session_error.ex#L1)

Raised when a session handle cannot be used for an operation.

The `:reason` field is the stable machine-readable failure contract:

- `:not_owner` means a structurally valid session handle was used outside the
  process named by its owner PID, whether the session is live, closed, or its
  owner has exited. Its exact message is
  `"Session is owned by another process"`.
- `:closed` means the owner tried to use a session it has already closed. Its
  exact message is `"Session is closed"`.
- `:already_handed_off` means an accepted handshake cannot be handed off
  again. Its exact message is `"Session was already handed off"`.
- `:unknown` means the value is not a known owner-local session handle.
  Legacy bare references from Decibel 0.2, malformed values, and well-shaped
  owner-local handles that Decibel did not issue use this reason. Its exact
  message is `"Unknown Decibel session"`.
- `:wrong_phase` means the session is live, but the operation is not valid for
  the current handshake turn or transport phase. Its exact message is
  `"Session operation <operation> requires <expected_phase> phase; current
  phase is <actual_phase>"`, with the bracketed values replaced by the
  corresponding atoms.

Validation first checks the handle shape, then compares its owner PID with
the calling process, then performs the owner-local state lookup, and finally
validates the phase. Consequently, a structurally valid handle used from
another process reports `:not_owner` before Decibel considers whether owner-
local state exists or was closed. This includes a handle retained after its
owner exits and a genuine-looking foreign handle.

This owner-PID-only classification is deliberate. Session handles are opaque
and must not be constructed or altered by callers. Decibel intentionally has
no handle registry, issuance proof, signature, or global verification state,
so a constructed or altered owner-local handle may report `:closed` rather
than `:unknown`.

For `:wrong_phase`, `:operation`, `:expected_phase`, and `:actual_phase`
identify the rejected transition. Those fields are `nil` for all other
reasons.

# `phase`

```elixir
@type phase() :: :handshake_write | :handshake_read | :handshake | :transport
```

The phase or handshake turn of a live session.

# `reason`

```elixir
@type reason() :: :not_owner | :closed | :already_handed_off | :unknown | :wrong_phase
```

The reason a session operation was rejected.

# `t`

```elixir
@type t() :: %Decibel.SessionError{
  __exception__: true,
  actual_phase: phase() | nil,
  expected_phase: phase() | nil,
  message: String.t(),
  operation: atom() | nil,
  reason: reason()
}
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
