Skip to content

Errors and sentinels

Every exported error value in the module, what raises it, and what fixes it. All of them are matched with the standard library's errors.Is; none requires phpboyscout/go/errors at the call site, even though the module builds its errors with it.

Construction errors

Raised by chat.New, chat.NewFallback* and chat.NewFileStore, before any request is made.

Sentinel Raised when What to do
chat.ErrUnableToConstruct construction failed outright and no client was returned fix the named cause; this is the one check every caller should make, because it separates "no client" from "a client missing a setting"
chat.ErrInvalidBaseURL Config.BaseURL fails any endpoint rule (too long, control characters, unparseable, carries userinfo, not https, no host, or a placeholder host) or is empty for ProviderOpenAICompatible correct the URL; put credentials in Config.Token, never in the URL
chat.ErrInvalidSnapshotID a snapshot identifier reaching FileStore.Save, Load or Delete is not a canonical lowercase-hex UUID, or resolves outside the store directory validate at your own boundary with chat.ValidateSnapshotID; snapshots from chat.NewSnapshot always pass

errors.Is(err, chat.ErrUnableToConstruct) is true whenever chat.New returned a nil client, including for every fatal cause listed on this page. The aggregate makes the sentinel a member so no type assertion is needed.

Request-time errors

Raised by a provider during Chat, Ask or StreamChat. Each wraps the vendor's own error, which stays reachable by unwrapping.

Sentinel Raised when What to do
chat.ErrThrottled the provider rate-limited the request and its own retries were exhausted back off, or configure a fallback composite; do not read the vendor's wording, which is often misleading (Gemini reports a per-minute burst limit as a quota and billing problem)
chat.ErrModelRejectedParameter the selected model refused a generation parameter that its provider structurally supports change Config.Model, or drop the parameter and use Config.Effort, which every provider accepts
chat.ErrMediaRejected an attachment is empty, over 20 MiB, of a type the sniffer cannot identify or the allowlist excludes, or declares a MIME family contradicting its bytes send a supported format, or downscale; the sniffed type is authoritative and a declared type can never override it
chat.ErrMediaUnsupported the selected provider accepts no media at all (claude-local), or not this attachment's type choose a provider that accepts the type (the table is in Defaults and limits)
chat.ErrCommandUnavailable a slash command needs a contract this client does not implement: /clear without PersistentChatClient, or /compact and /undo without TranscriptEditor upgrade the provider module, or drop the command from your CommandSet; /help lists it either way, so a user is told rather than left guessing
chat.ErrTranscriptMoved another goroutine changed the conversation while a policy was being applied, so the edit was computed against turns that no longer exist retry; it is not a failure. The client cannot hold its lock across a policy that calls the model, so this window is unavoidable, and applying the edit anyway would silently drop the turns that arrived
chat.ErrStatelessRestore Restore is called on a client built with Config.Stateless restore into a conversational client; a stateless client retains no conversation for a snapshot to land in. Save still works and returns a snapshot with no messages

Media errors are raised before any network call, because the attachment is validated at the boundary, so a rejected attachment costs nothing.

The ConfigErrors aggregate

chat.New returns problems as one *chat.ConfigErrors rather than the first one it hit, so a caller with three mistakes learns all three from one call.

Member Returns Use it to
Error() string one line naming every problem, prefixed by unable to construct client when fatal and configuration could not be fully applied when not log it
Unwrap() []error the individual problems, plus ErrUnableToConstruct when fatal let errors.Is and errors.As reach every member
Dropped() []DroppedSetting the settings that were not applied, each with Fields, Capability and Reason decide whether the shortfall matters to this job
Fatal() bool whether construction failed outright the cheap equivalent of errors.Is(err, ErrUnableToConstruct) when you already hold the type
var cfgErr *chat.ConfigErrors
if errors.As(err, &cfgErr) {
    for _, d := range cfgErr.Dropped() {
        log.Info("not applied", "fields", d.Fields, "capability", d.Capability, "why", d.Reason)
    }
}

Reading hints

Errors from this module carry hints, the half that says what to do rather than what is wrong. Read them with the standard readers from phpboyscout/go/errors:

import "gitlab.com/phpboyscout/go/errors"

for _, h := range errors.GetAllHints(err) {
    log.Info("hint", "advice", h)
}

Both GetAllHints and FlattenHints descend into an aggregate, so they work on anything chat.New returns.

chat.Hints was removed in v0.9.0

It existed because cockroachdb/errors walked single unwraps only, so hints below an aggregate vanished silently. go/errors has one chain traversal that descends into a multi-error, which removed the reason for the helper rather than fixing it. Consumers reading hints should move to the readers above.

Tagging a provider error: chat.MarkAs

Provider modules tag their vendor errors with a sentinel using chat.MarkAs(err, chat.ErrThrottled). Both of these then hold under the standard library:

errors.Is(marked, chat.ErrThrottled) // true
errors.Is(marked, providerErr)       // true

A nil error returns nil, so a call site can wrap unconditionally.

The helper exists because of a trap the module walked into once: annotation libraries have historically offered a Mark whose markers only their own Is recognises, so a consumer writing the idiomatic standard-library check gets false. MarkAs wraps with %w: %w instead, which the standard library understands. phpboyscout/go/errors offers no Mark, so the trap is not reachable today. The helper stays because the guarantee it makes is the one callers depend on.

Failures that carry no sentinel

These are returned as plain messages. There is nothing to match on beyond the class the aggregate gives them, so treat the text as diagnostic, not as an API.

Message Raised when What it usually means
unsupported provider: <name> no factory is registered under that name the provider module's blank import is missing, or the name is misspelled
claude binary not found in PATH: … ProviderClaudeLocal cannot find the claude CLI install Claude Code and authenticate it once
Model is required for ProviderOpenAICompatible: … ProviderOpenAICompatible was configured without Config.Model name the backend's model, e.g. llama3.2 for Ollama
Anthropic API key is required but not provided, OpenAI token is required but not provided, Gemini API key is required but not provided the whole credential cascade resolved to nothing export the provider's well-known variable, or set Config.Token or Config.Credentials
ProviderClaudeLocal does not support SetTools in this version tools are registered on claude-local use an API provider for tool calling
provider <name> does not support Config.Stateless the provider module predates chat.StatelessCapable upgrade the provider module; do not unset the flag
provider <name> cannot carry Config.Temperature or Config.TopP the provider has no sampling concept at any model (claude-local is the case) use Config.Effort, or change provider
provider <name> cannot carry Config.Effort the provider module predates chat.EffortCapable upgrade the provider module
provider <name> cannot carry Config.CacheTTL the provider module does not implement chat.CachingChatClient upgrade the provider module, or unset Config.CacheTTL
encryption key must be 32 bytes, got <n> chat.WithEncryption was given the wrong key length generate one with chat.GenerateEncryptionKey
fallback: at least one provider config is required a fallback constructor was handed an empty slice supply at least the primary
fallback: no provider could be constructed every member of a composite failed to build; marked ErrUnableToConstruct fix the credentials or imports the error names
fallback refused: a tool call has executed in this conversation WithStrictToolContext is set and a tool ran before the failure accept lossy text-only failover by removing the option, or handle the error
<Provider> reached maximum ReAct steps (<n>) without a final answer the tool loop ran Config.MaxSteps times and the model never produced a final text answer raise Config.MaxSteps, or narrow the task (the call returns no partial text)

The rows that describe a construction failure (the unsupported provider, the missing claude binary, the four "cannot carry" messages, and fallback: no provider could be constructed) all satisfy errors.Is(err, chat.ErrUnableToConstruct), so that check stays reliable even though the specific cause has no sentinel of its own. The remaining rows do not: SetTools on claude-local and fallback refused are returned from a call rather than from construction, and the chat.NewFileStore key-length error and the empty-slice fallback error are returned bare.