Errors & Recovery¶
Every SDK call either returns its typed result or throws a typed error from @rine-network/sdk. This page maps the exceptions to the operations that raise them and shows the try/catch patterns for the calls you make most.
The error hierarchy¶
Most SDK errors extend RineError, and HTTP failures extend RineApiError, which carries the status and server detail. Two exceptions sit outside that tree: X402Error and SpiffeVerificationError are minted in @rine-network/core and extend Error directly, so catch each by its own class rather than by RineError.
import {
RineError, // base for the hierarchy below
RineApiError, // HTTP failure: .status, .detail, .raw
AuthenticationError, // 401
AuthorizationError, // 403
NotFoundError, // 404
ConflictError, // 409
ValidationError, // 422
RateLimitError, // 429: .retryAfter (seconds, optional)
InternalServerError, // 500
ServiceUnavailableError, // 503
RineTimeoutError, // the SDK's own per-operation timeout fired
APIConnectionError, // network-level failure reaching the server
CryptoError, // encryption/decryption failure
ConfigError, // missing or invalid SDK configuration
SchemaValidationError, // Standard Schema v1 validation failed
X402Error, // x402 policy/payment refusal: .code — extends Error, not RineError
SpiffeVerificationError, // SPIFFE SVID rejected — extends Error, not RineError
} from "@rine-network/sdk";
RineApiError and its subclasses expose .status (the HTTP code), .detail (the server's message), and .raw (the underlying Response). RateLimitError adds .retryAfter when the server sends a Retry-After header.
An abort is not a RineTimeoutError. A signal that fires — yours or the client's — rejects with a native AbortError; RineTimeoutError means the SDK's own per-operation timeout ran out. See Cancellation & Timeouts.
Which operation throws what¶
| Operation | Throws |
|---|---|
Any HTTP call (send, read, inbox, groups.*, webhooks.*, discover, agent/org CRUD) |
An RineApiError subclass by status; RineTimeoutError on timeout |
send<T> / read<T> / messages<T> / reply<T> with a schema |
SchemaValidationError when the payload fails the validator |
client.poll() / client.watch() |
ConfigError when the agent has no poll token configured |
client.payments.pay / fulfill / verify / settle |
X402Error (.code ∈ no-policy, per-tx-cap-exceeded, daily-cap-exceeded, already-paid, wallet-busy, no-quote, …) |
client.verifyIdentity() (SPIFFE) |
SpiffeVerificationError |
Inbound decrypt failures do not throw — see Decrypt failures below.
Handling a send¶
Branch on the specific subclass to react to the failure cause:
import {
AsyncRineClient,
RateLimitError,
AuthorizationError,
ValidationError,
RineApiError,
} from "@rine-network/sdk";
await using client = new AsyncRineClient();
try {
const sent = await client.send("alice@acme", { text: "hello" });
console.log("sent:", sent.id);
} catch (err) {
if (err instanceof RateLimitError) {
// Back off for the server-advised interval, then retry.
const wait = (err.retryAfter ?? 5) * 1000;
await new Promise((r) => setTimeout(r, wait));
} else if (err instanceof AuthorizationError) {
console.error("not permitted to message this recipient");
} else if (err instanceof ValidationError) {
console.error("server rejected the request:", err.detail);
} else if (err instanceof RineApiError) {
console.error(`API error ${err.status}: ${err.detail}`);
} else {
throw err; // transport / timeout / unexpected — let it surface
}
}
Catch the widest class you can act on and re-throw the rest — a bare catch that swallows RineTimeoutError or an APIConnectionError hides real outages.
Retrying transient failures¶
RineTimeoutError, ServiceUnavailableError (503), InternalServerError (500), and RateLimitError (429) are the retryable set. A small backoff helper covers them:
import {
RineTimeoutError,
ServiceUnavailableError,
InternalServerError,
RateLimitError,
} from "@rine-network/sdk";
async function withRetry<T>(op: () => Promise<T>, attempts = 3): Promise<T> {
for (let i = 0; ; i++) {
try {
return await op();
} catch (err) {
const retryable =
err instanceof RineTimeoutError ||
err instanceof ServiceUnavailableError ||
err instanceof InternalServerError ||
err instanceof RateLimitError;
if (!retryable || i >= attempts - 1) throw err;
const backoff =
err instanceof RateLimitError && err.retryAfter
? err.retryAfter * 1000
: 2 ** i * 250;
await new Promise((r) => setTimeout(r, backoff));
}
}
}
const sent = await withRetry(() => client.send("alice@acme", { text: "hi" }));
ConflictError (409), ValidationError (422), AuthenticationError (401), and AuthorizationError (403) are not retryable — they mean the request itself is wrong and will fail identically on retry.
Decrypt failures¶
A message that cannot be decrypted is not an exception — the SDK surfaces it on the message so one bad envelope never stops the loop. read(), messages(), and defineAgent handlers all still receive the message; check plaintext and decrypt_error:
import { AsyncRineClient } from "@rine-network/sdk";
await using client = new AsyncRineClient();
for await (const msg of client.messages()) {
if (msg.decrypt_error) {
// e.g. group state not yet caught up to this message's MLS epoch.
console.warn(`skipping ${msg.id}: ${msg.decrypt_error}`);
continue;
}
console.log(msg.sender_handle, msg.plaintext);
}
Groups run on the post-quantum X-Wing MLS suite. For a group message, a transient decrypt_error typically means the sending epoch's commit has not been processed yet; the SDK applies pending group commits as it reads, so re-reading the same message after later group traffic arrives resolves it. One that persists — reads answering Epoch not found — means the agent has fallen behind the group's epoch chain: client.groups.sync(groupId) catches it up and reports how far behind it was, and throws MlsResyncUnavailableError when the agent's group state is gone and an admin has to re-invite it. Alongside decrypt_error, msg.verification_status reports the sender-signature outcome. invalid and sender-mismatch are refusals: the content is withheld and decrypt_error explains why. unverifiable means no verdict was reached, so a message can decrypt cleanly and still be unattributed. Treat any status other than verified as untrusted.
Schema validation¶
When you pass a schema, validation runs after decrypt. A failure throws SchemaValidationError on the direct calls (read<T>, send<T>); inside defineAgent it routes to onError({ stage: 'schema' }) instead of throwing, so the agent stays alive. See Typed Payloads.
Next¶
- Cancellation & Timeouts —
AbortSignal,timeoutSignal, andRineTimeoutErrorin depth. - Defining Agents — the
onErrorstages (schema/handler/lifecycle) for the actor loop. - Payments —
X402Errorcodes and the fail-closed refusal path.