Errors
Seven domain error classes with structured hints and documentation URLs.
usertrust defines seven domain-specific error classes. All extend Error and include structured hint and docsUrl properties for actionable diagnostics.
Catching Errors
import {
InsufficientBalanceError,
PolicyDeniedError,
LedgerUnavailableError,
} from "usertrust";
try {
const { response, receipt } = await client.messages.create({
model: "claude-fable-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello" }],
});
} catch (err) {
if (err instanceof InsufficientBalanceError) {
console.log(`Need ${err.required} UT, have ${err.available} UT`);
console.log(err.hint);
}
if (err instanceof PolicyDeniedError) {
console.log(`Blocked: ${err.reason}`);
}
if (err instanceof LedgerUnavailableError) {
console.log(`TigerBeetle down: ${err.cause_message}`);
}
}Every error class follows the same pattern: a descriptive message, a hint with a suggested fix, and a docsUrl pointing to detailed documentation.
InsufficientBalanceError
Thrown when the requested LLM call would exceed the remaining budget.
class InsufficientBalanceError extends Error {
readonly userId: string;
readonly required: number; // usertokens needed
readonly available: number; // usertokens remaining
readonly hint: string;
readonly docsUrl: string;
readonly auditEventHash?: string; // the `ledger_rejected` event's hash
readonly auditDegraded?: boolean; // the append failed and was dead-lettered
}When it fires: During the PENDING hold phase, before the LLM call is forwarded to the provider. No tokens are consumed.
Hint: "Increase the budget in trust() options or add funds via the ledger."
PolicyDeniedError
Thrown when a policy rule or PII detector blocks the request.
class PolicyDeniedError extends Error {
readonly reason: string;
readonly hint: string;
readonly docsUrl: string;
readonly auditEventHash?: string; // the `policy_denied` event's hash
readonly auditDegraded?: boolean; // the append failed and was dead-lettered
}When it fires: During policy evaluation, before the LLM call. The reason field describes which rule or PII type triggered the denial.
Hint: 'Check your policy rules in .usertrust/policies/ or use { pii: "warn" } to downgrade PII enforcement.'
The denial correlation handle
Both denial errors above carry auditEventHash — the hash of the chain event
the governor wrote when it refused the call. It is the join between the
exception your code logged and the record an auditor reads: policy_denied for
a governor decision (a policy rule, a budget-family rule, PII, injection, or an
unpriceable model), ledger_rejected for the ledger's own atomic refusal of the
hold.
try {
await client.messages.create({ /* ... */ });
} catch (err) {
if (err instanceof PolicyDeniedError) {
// Quote this in your incident ticket; `usertrust verify` can find it.
console.log(`denied, chain event ${err.auditEventHash ?? "(none)"}`);
}
}The two fields answer different questions, and you need both:
auditEventHash | auditDegraded | What happened |
|---|---|---|
| set | absent | The denial is on the chain, at that hash. |
| set | true | The event is on the chain at that hash, but the write reported failure — the log was fsync'd and the .meta sidecar was not. The handle is usable; run usertrust verify on the vault. |
| absent | true | The append was attempted, nothing durable landed, and the payload was dead-lettered to .usertrust/dlq/. There is no chain record. |
| absent | absent | No append was attempted — an error built by hand, or thrown by a version before denial events existed. |
A failed denial-append never changes the error you receive. audit.failClosed
exists to stop an unaudited spend from settling; a denial already refused
the call and moved no money, so escalating here would only replace an
actionable, typed denial with an AuditDegradedError and hide why the call
was refused.
Indirect callers do not receive the handle yet. usertrust-server and the
ACS adapter convert the error to a wire response and do not forward
auditEventHash today; the field is on the in-process error object only.
AccountNotFoundError
Thrown when the TigerBeetle account for the given user does not exist.
class AccountNotFoundError extends Error {
readonly userId: string;
readonly hint: string;
readonly docsUrl: string;
}When it fires: During ledger initialization or the first spend attempt.
Hint: 'Run "npx usertrust init" to create accounts, or verify the userId matches your config.'
IdempotencyConflictError
Thrown when a transfer with the same ID has already been submitted to TigerBeetle.
class IdempotencyConflictError extends Error {
readonly key: string;
readonly hint: string;
readonly docsUrl: string;
}When it fires: During the PENDING hold phase, if the generated transferId collides with an existing transfer.
Hint: "This transfer was already submitted. Use a unique transferId for retries."
LedgerUnavailableError
Thrown when TigerBeetle is unreachable. The LLM call is not forwarded to the provider.
class LedgerUnavailableError extends Error {
readonly cause_message: string;
readonly hint: string;
readonly docsUrl: string;
}When it fires: Before any LLM call, during the connection check or PENDING hold attempt. This is failure mode 15.4 -- no tokens are at risk because no hold was placed.
Hint: 'Start TigerBeetle with "npx usertrust tb start" or use { dryRun: true } to skip the ledger.'
That hint is the error's own text, quoted verbatim — and usertrust tb start is
a stub that prints a manual command rather than starting anything. Follow the
CLI page's usertrust tb section for the command that
actually starts a node, or pass { dryRun: true } to skip the ledger entirely.
When TigerBeetle is unreachable, usertrust refuses to forward the request. This is by design -- without a ledger, budget enforcement is impossible.
AuditDegradedError
Thrown (or flagged) when the audit chain write fails after the LLM call and ledger settlement both succeed.
class AuditDegradedError extends Error {
readonly cause_message: string;
readonly hint: string;
readonly docsUrl: string;
}When it fires: After the LLM call succeeds and the ledger POST settles. This is failure mode 15.3. The response is still returned to the caller with auditDegraded: true on the receipt. The failed event is written to the dead-letter queue.
Hint: "Check disk space and permissions on the .usertrust/audit/ directory."
usertrust never fails the user on audit degradation. If the LLM call succeeded and the ledger settled, the response is always returned.
VaultNotInitializedError
Thrown when the .usertrust/ vault directory does not exist.
class VaultNotInitializedError extends Error {
readonly path: string;
readonly hint: string;
readonly docsUrl: string;
}When it fires: During trust() initialization, before any LLM calls.
Hint: 'Run "npx usertrust init" to create the vault directory.'
Error Summary
| Error | Failure Mode | LLM Call Forwarded? | Tokens At Risk? |
|---|---|---|---|
InsufficientBalanceError | Pre-call | No | No |
PolicyDeniedError | Pre-call | No | No |
AccountNotFoundError | Pre-call | No | No |
IdempotencyConflictError | Pre-call | No | No |
LedgerUnavailableError | 15.4 | No | No |
AuditDegradedError | 15.3 | Yes (succeeded) | No (settled) |
VaultNotInitializedError | Pre-call | No | No |
All pre-call errors are thrown before any interaction with the LLM provider. No tokens are consumed and no budget is affected.