Handle errors
A fault is data, not a transport failure
Section titled “A fault is data, not a transport failure”A refusal arrives as a normal HTTP 200 whose tool result carries isError: true. The server
answered; the answer was a refusal. Only a protocol-level problem uses the JSON-RPC error channel.
This matters for retries: a refusal re-earns itself. Retrying a validation fault gets you the
same fault having spent a round trip to prove it. Repair the call instead — field names what to fix
and details carries the allowed values when the server can enumerate them.
The four categories
Section titled “The four categories”| Category | Example | What to do |
|---|---|---|
| Auth | unauthenticated, forbidden | Refresh or fix the credential. Never retry in a loop — that attacks your own auth service |
| Validation | validation | The request is wrong. Fix the call site; retrying cannot help |
| Fault, retryable | rate_limited, dependency_unavailable, internal | The SDK already retried. Reaching you means exhaustion |
| Transport | connection refused, timeout | The platform did not answer. Not a refusal |
switch {case errors.Is(err, vectadyne.ErrUnauthenticated):case vectadyne.IsFaultCode(err, wire.FaultCodeValidation):case errors.Is(err, vectadyne.ErrFault):case err != nil: // transport}The distinction that matters most
Section titled “The distinction that matters most”“The platform said no” is not “the platform did not answer.”
The SDK never synthesizes a verdict. If the control plane is unreachable you get a typed
transport error, not an invented allow.
That is a deliberate cost, and worth stating plainly rather than dressing up:
- You are coupled to the outage. There is no local cache and no last-known-good context.
- You wait first. Up to the total timeout, including retries and backoff, before finding out.
- The decision is yours, and has to be written down. Stop, queue, degrade a feature, or proceed and record that you did — the SDK will not choose.
Why not a fallback verdict? Because a synthesized allow is an ungoverned decision wearing the
platform’s name, and it would be indistinguishable in your logs from a real one. Moving “proceed on
outage” into your application code does not make it governed — but it makes it visible and
reviewable, which is the honest version.
Retries, and what is never retried
Section titled “Retries, and what is never retried”The SDK retries on transport outcome only. It never reads decision, degraded or any governed
payload to make that call.
| Outcome | Retried? |
|---|---|
| transport error before a response | yes |
429 rate limited | yes, after Retry-After |
503, 500 | yes, with backoff |
400 validation | no |
401, 403 | no |
404, 409, 410 | no |
413 payload too large | no — shrink the batch |
| any verdict, any decision | no — it is a successful answer |
A block producing two requests is a bug. Assert the request count.
Rate limits
Section titled “Rate limits”RateLimitedError carries retry_after typed, because it is the one field that says what to do
next. The SDK waits the server’s figure rather than applying its own backoff — the server knows when
the bucket refills.
One asymmetry to know about: a 429 from the token endpoint uses the RFC 6749 OAuth error shape,
not our fault envelope, and puts its correlation id only in the X-Request-Id header. The SDKs
normalise both into the same typed error so you do not parse two shapes.
Timeouts come in two
Section titled “Timeouts come in two”| Default | Why both | |
|---|---|---|
| Per-attempt | 30s | Without it, one hung connection eats the whole budget |
| Total | 60s | Without it, N retries multiply into an unbounded wait |
Your own context or cancellation deadline always wins over both. Backoff sleeps consume the total budget — a client that sleeps outside its own deadline has no deadline.
Do not confuse either with latency_budget_ms, which is a server-side budget on
context.assemble that degrades rather than failing.
Always log the request_id
Section titled “Always log the request_id”Every response and every fault carries one. It is what makes a bug report actionable, and it is safe
to log — unlike prompt_block or memory content, which no SDK logs at any level.