The SDK throws two error classes. Auth failures are their own class so you can handle them up front; everything else is BeliefsError with a structured code.
1import Beliefs, { BetaAccessError, BeliefsError } from 'beliefs'BetaAccessError
The API key is missing, invalid, or revoked, or your account isn't on the beta allowlist (HTTP 401/403). Surfaces a signupUrl for self-service requests.
1try {
2 await beliefs.before(input)
3} catch (err) {
4 if (err instanceof BetaAccessError) {
5 console.log(err.signupUrl) // 'https://thinkn.ai/waitlist'
6 return redirect(err.signupUrl)
7 }
8 throw err
9}| Property | Type | Notes |
|---|---|---|
name | string | 'BetaAccessError' |
message | string | Human-readable reason |
status | 401 | 403 | HTTP status from the engine |
signupUrl | string | Where to request access |
Not retryable. Either the key works or it doesn't. Retrying won't change the answer.
BeliefsError
Everything else. Carries a structured code, an HTTP status, and a retryable flag. The SDK auto-retries transient errors (429, 5xx) with exponential backoff, so you only see these after retries are exhausted.
1try {
2 await beliefs.after(result.text)
3} catch (err) {
4 if (err instanceof BeliefsError) {
5 console.log(err.code) // 'rate_limit/exceeded'
6 console.log(err.status) // 429
7 console.log(err.retryable) // true (already retried, exhausted)
8 }
9 throw err
10}| Property | Type | Notes |
|---|---|---|
name | string | 'BeliefsError' |
message | string | Human-readable reason |
code | string | Stable, namespaced identifier (see catalog) |
status | number | HTTP status from the engine |
retryable | boolean | true if the SDK retried before throwing |
details | Record<string, unknown> | Optional structured context (request id, validation issues, etc.) |
Code catalog
Error codes are namespaced as <category>/<reason>. Categories are stable; reasons may grow.
auth/*
| Code | HTTP | Retryable | Meaning | Action |
|---|---|---|---|---|
auth/missing_key | 401 | No | Constructor was called without apiKey or scopeToken | Provide one. See Auth. |
auth/invalid_key | 401 | No | Key format is malformed or unknown to the server | Verify the value of BELIEFS_KEY. |
auth/revoked_key | 403 | No | Key was valid but revoked (rotated, deleted, or org disabled) | Issue a new key from your profile. |
auth/scope_token_expired | 401 | No | A short-lived scopeToken passed its exp claim | Mint a fresh token server-side. |
For 401/403 from the beta allowlist specifically, expect BetaAccessError, not BeliefsError.
validation/*
| Code | HTTP | Retryable | Meaning | Action |
|---|---|---|---|---|
validation/invalid_json | 400 | No | Request body failed JSON parsing | Check serialization. Usually a non-stringifiable value in add() or after() payload. |
validation/invalid_shape | 400 | No | Request shape failed engine schema validation | err.details.issues lists the Zod issues. |
validation/missing_field | 400 | No | A required field was omitted | err.details.field names the field. |
rate_limit/*
| Code | HTTP | Retryable | Meaning | Action |
|---|---|---|---|---|
rate_limit/exceeded | 429 | Yes (auto-retried) | Per-key or per-namespace quota exceeded | Inspect err.details.retryAfter. Drop call rate or upgrade. |
internal/*
| Code | HTTP | Retryable | Meaning | Action |
|---|---|---|---|---|
internal/error | 5xx | Yes (auto-retried) | Engine error, retried with backoff before surfacing | If repeated, check status page; file a P1 with err.details.requestId. |
internal/timeout | 504 | Yes (auto-retried) | Engine took longer than the configured timeout | Retry, or split into smaller observations. |
Operation-specific
| Code | HTTP | Retryable | Meaning | Action |
|---|---|---|---|---|
remove_where/unsupported_source | 400 | No | removeWhere({ source }) was called with a kind other than block:<id> | Use retract() or remove(). Engine support for agent:, thread:, source: is in flight. |
Retry & backoff
The SDK auto-retries 429 and 5xx responses with exponential backoff before throwing. By the time you see a BeliefsError with retryable: true, the SDK has already exhausted its retry budget. You don't need to wrap it in a retry loop yourself.
retryable: false errors (auth, validation) are never retried. Don't retry them in your handler. Fix the input.
Handling pattern
A single try/catch covers both classes. Branch on instance, then on code if you need fine-grained handling.
1import Beliefs, { BetaAccessError, BeliefsError } from 'beliefs'
2
3try {
4 const context = await beliefs.before(userMessage)
5 // ... agent run, after, etc.
6} catch (err) {
7 if (err instanceof BetaAccessError) {
8 // Send the user to the waitlist
9 return { error: 'beta_access_required', signupUrl: err.signupUrl }
10 }
11
12 if (err instanceof BeliefsError) {
13 if (err.code.startsWith('auth/')) {
14 // Auth misconfiguration: log and surface to ops
15 console.error('beliefs auth failure', err.code, err.message)
16 return { error: 'auth_misconfigured' }
17 }
18
19 if (err.code === 'rate_limit/exceeded') {
20 // Already auto-retried; back off in your application loop
21 return { error: 'rate_limited', retryAfter: err.details?.retryAfter }
22 }
23
24 if (err.code.startsWith('validation/')) {
25 // Bug in the calling code: log and fix
26 console.error('beliefs validation failure', err.details)
27 return { error: 'invalid_request' }
28 }
29
30 // internal/*: already retried, surface as transient
31 return { error: 'beliefs_unavailable' }
32 }
33
34 // Not a beliefs error: re-throw for upstream handling
35 throw err
36}