Errors and recovery
The seven typed errors the SDK raises, what each means operationally, and the right recovery for each.
Updated May 29, 2026
The SDK raises seven typed errors. Six map to marketing-spec refusals. The seventh, NotLiveYetError, is the v0.1.0 surface: every HttpTransport method raises it until the auth server is live. All seven carry enough state on the error object that a well-written agent can recover without a second round-trip. The auth layer enforces the refusals, not the prompt layer.
The catalog
| Error | HTTP | Retryable | Surfaces |
|---|---|---|---|
NotLiveYetError | n/a | no | every HttpTransport call in v0.1.0 |
AuthError | 401 | no | every call |
ScopeError | 403 | no | every call |
RateLimitError | 429 | yes, after retryAfterSeconds | every call |
CommitNotAllowedError | 403 | no | only if an agent attempts a clinician-only commit path |
ClinicianRejectedDraftError | 403 | no | commit calls on a draft a clinician already rejected |
InstrumentFrozenError | 409 | no | any mutation of PHQ-9 or GAD-7 |
There is no generic NyraError to catch everything. The base class exists for instanceof, but the SDK never raises it directly. Catch the typed subclass, or rethrow.
NotLiveYetError is the one error you can stop catching once the auth server ships. The other six are permanent surface.
Anatomy
Every typed error carries the same base fields:
type NyraErrorBase = {
name: string; // matches the class name verbatim
message: string; // one-line, human-readable
requestId: string; // server-issued; quote it when filing support
auditId?: string; // present when the refusal landed in the audit log
};
Subclasses extend with the state the recovery path needs:
type NotLiveYetError = NyraErrorBase & { method: string; sdkVersion: string };
type RateLimitError = NyraErrorBase & { bucket: "evidence_map" | "simulation" | "commit"; retryAfterSeconds: number };
type ScopeError = NyraErrorBase & { missingScope: string };
type CommitNotAllowedError = NyraErrorBase & { attemptedAction: string };
type ClinicianRejectedDraftError = NyraErrorBase & { rejectedDraftId: string; rejectedAt: string };
type InstrumentFrozenError = NyraErrorBase & { instrument: "phq9" | "gad7"; instrumentVersion: string };
Python mirrors the same field set on the equivalent classes, in snake_case. A golden fixture in both test suites pins the field set so cross-language drift fails in CI.
Recovery, by error
| Error | What to do |
|---|---|
NotLiveYetError | wire the shape; defer the call until the auth server ships; cite sdkVersion if you log it |
AuthError | refresh the token; if refresh fails, re-authenticate end-to-end |
ScopeError | surface missingScope to the operator; do not silently retry |
RateLimitError | sleep retryAfterSeconds against the bucket, then retry the exact call |
CommitNotAllowedError | never; the agent must hand the draft to a clinician |
ClinicianRejectedDraftError | drop the draft; do not re-propose verbatim; cite rejectedDraftId in the next proposal |
InstrumentFrozenError | abandon the mutation; PHQ-9 and GAD-7 are version-pinned and immutable from the SDK |
The two errors an agent will see most often are RateLimitError and CommitNotAllowedError. Both have runnable examples below.
Backing off a rate limit
import { RateLimitError } from "@humyn/nyra";
async function runWithBackoff<T>(call: () => Promise<T>, maxAttempts = 3): Promise<T> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await call();
} catch (err) {
if (err instanceof RateLimitError && attempt < maxAttempts) {
await new Promise((r) => setTimeout(r, err.retryAfterSeconds * 1000));
continue;
}
throw err;
}
}
throw new Error("unreachable");
}
from humyn_nyra import RateLimitError
async def run_with_backoff(call, max_attempts: int = 3):
for attempt in range(1, max_attempts + 1):
try:
return await call()
except RateLimitError as err:
if attempt == max_attempts:
raise
await asyncio.sleep(err.retry_after_seconds)
The backoff is per-bucket. If your agent reads the evidence map and runs simulations concurrently, hold one limiter per bucket so a simulation backoff does not stall an unrelated read.
Catching a commit refusal
import { CommitNotAllowedError } from "@humyn/nyra";
try {
await client.createDraft({ patientId, body });
} catch (err) {
if (err instanceof CommitNotAllowedError) {
// Hand the draft state to the clinician's surface. Do not retry from the agent.
return { status: "needs_clinician", attemptedAction: err.attemptedAction };
}
throw err;
}
from humyn_nyra import CommitNotAllowedError
try:
await client.create_draft(patient_id=patient_id, body=body)
except CommitNotAllowedError as err:
return {"status": "needs_clinician", "attempted_action": err.attempted_action}
The agent's correct response to CommitNotAllowedError is structural, not behavioral. Hand the artifact off. A retry loop here is a bug.
What never throws
A few situations look like errors but are not. The SDK returns an empty or default-shaped value instead, so the agent does not need to branch on exceptions for routine state.
- An evidence map with no signal yet returns the full shape with empty arrays and a
confidence.overallof0. It does not throw. - A sandbox with no drafts returns
[]fromlistDrafts. It does not throw. - A reflection with no transcript yet (the patient is mid-write) returns
nullfrom the read. It does not throw. - A simulation that the surrogate cannot bound returns a confidence band of
{ low: 0, high: 1 }andconfidence.reason: "unbounded". It does not throw.
Anything else that fails raises one of the six typed errors above. There is no untyped Error path inside the SDK.
Next: Webhooks and realtime events.