Skip to content

Reference

Errors

Exception hierarchy exported from @alter-ai/alter-sdk.

Every public exception thrown by the SDK extends AlterSDKError. The hierarchy mirrors the Python SDK one-for-one — same class names, same semantics, same fields.

Version compatibility: this page tracks the latest published SDK line. CallerAgentMismatchError, CallerAgentResolutionError, KeyExpiredError, KeyRotationExpiredError, KeyInactiveError, and KeyAppNotFoundError require @alter-ai/alter-sdk>=0.23.1. Version 0.22.0 does not export them; upgrade before importing or checking instanceof against these classes. MemoryIsolationError, its code property and the MEMORY_ISOLATION_CODES vocabulary are unreleased: they ship in the next release after 0.27.0, which does not export them.

For the canonical error code → exception class mapping and recovery guidance, see Errors.

import {
NoDelegatedGrantError,
QuotaExceededError,
RateLimitError,
ScopeReauthRequiredError,
} from "@alter-ai/alter-sdk";
const makeRequest = () =>
app.request("GET", url, { provider: "<provider-id>" });
// Bounded retries on a throttle: honour `retryAfter` when the response carried
// one, otherwise back off exponentially; add jitter so callers spread out.
async function withRateLimitRetry<T>(
call: () => Promise<T>,
maxAttempts = 5,
): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await call();
} catch (error) {
if (!(error instanceof RateLimitError) || attempt >= maxAttempts) {
throw error;
}
// `scope` says WHICH ceiling was hit: spreading load across keys helps
// an `api_key` throttle and does nothing for an `organization` one. It is
// undefined for a non-JSON 429 from an intermediary.
console.warn(`Throttled on ${error.scope ?? "an unidentified limiter"}`);
const waitSeconds = error.retryAfter ?? Math.min(60, 2 ** attempt);
const jitterSeconds = Math.random() * waitSeconds * 0.2;
await new Promise((r) =>
setTimeout(r, (waitSeconds + jitterSeconds) * 1000),
);
}
}
}
try {
await withRateLimitRetry(makeRequest);
} catch (error) {
if (error instanceof NoDelegatedGrantError) {
const session = await app.createConnectSessionForError(error);
redirectUser(session.connectUrl);
return;
}
if (error instanceof ScopeReauthRequiredError) {
// The user must re-authorize with wider provider scopes.
console.warn(`Missing scopes: ${error.missingScopes?.join(", ")}`);
}
if (error instanceof QuotaExceededError) {
// Request-quota policy denial — the SDK never auto-retries it.
await new Promise((r) => setTimeout(r, (error.retryAfter ?? 60) * 1000));
return makeRequest();
}
throw error;
}

Every name imported above is used by the snippet, so it compiles as-is under noUnusedLocals. The classes documented below that it does not branch on (AlterSDKError, GrantNotFoundError, GrantNotDelegableError, …) are deliberately not imported here — add them when you add a branch for them.

  • AlterSDKError — root of the hierarchy. Every other exception extends this.
  • AlterValueError — input validation failed at the SDK boundary. Distinct from BackendError so callers can branch on “I passed something bad” vs “the backend said no.”
  • MemoryIsolationError — the private-memory identity or partition could not be verified. A direct child of AlterSDKError, thrown locally by memoryScope(), ScopedMemoryStore and verifyIdentityAssertion(), never from an HTTP response. Branch on code (a MemoryIsolationCode, one of the exported MEMORY_ISOLATION_CODES); details.reason carries a coarse category for memory_assertion_invalid. See Errors for the code table.
  • BackendError — generic backend failure. details.status_code is populated when an HTTP status code is the proximate cause.
  • ServerFeatureUnsupportedError — extends BackendError; no extra fields. The Alter backend predates a feature the call used: agent.createManagedSecretConnectSession() against a backend that still requires delegated_agent_id (its request-validation 422 names that field as missing), or delegable: true on createManagedSecretConnectSession when the backend did not echo delegable_requested, or echoed it as false. The SDK throws instead of returning a session that would not do what was asked. details.feature names what is missing: agent_managed_secret_connect_session or managed_secret_connect_delegable. Retrying against the same backend cannot help: until it supports the feature, request the session with an app key — app.getAgent(agentId).createManagedSecretConnectSession(...) or the App method — without delegable.
  • RestrictedGrantRequiresProxyError — the grant is restricted to the proxy call path; use proxyRequest().
  • SiblingLabelConflictError — the sibling label is already in use on this connection; pick another label.
  • ReAuthRequiredError — the user must re-authorize via Alter Connect. Parent class of the grant-state errors below (GrantExpiredError, GrantRevokedError, GrantDeletedError, CredentialRevokedError), so one instanceof catch covers every “send the user back through Connect” condition.
  • GrantExpiredError — the grant’s TTL has elapsed. Exposes providerId, agentId, appUserId, populated only on the delegation-path expiry (a TTL-lapsed delegation chain); undefined on the root-path TTL expiry.
  • GrantRevokedError — the grant was explicitly revoked (the end user revoked it in the Wallet or disconnected the connection, or an administrator revoked it). Raised on every subsequent App-side call that addresses the revoked grant. Agent callers — and calls addressing a delegated (child) grant — never receive this class: for them a revoked grant surfaces as the reason-neutral GrantDeletedError, so a delegated agent cannot probe whether its access was deliberately revoked.
  • GrantDeletedError — the grant existed but is permanently gone with no more specific terminal signal (for example, a TTL-expired grant addressed again, a grant whose underlying credential was torn down, or any revoked grant addressed from the Agent path). Re-authorization issues a new grant id.
  • GrantNotFoundError
  • AgentDelegationMissingError — extends GrantNotFoundError (so an instanceof GrantNotFoundError catch still fires). Thrown on the agent path when request(method, url, { grantId }) / proxyRequest({ grantId }) hits a grant_not_found — the grant is not delegated to this agent, or a user/base grant id was passed where the agent’s own delegation id (listGrants) was required. Recover by delegating the agent through Connect, or resolve by provider (omit grantId). The App/operator path is unaffected.
  • GrantNotDelegableError — extends BackendError. Thrown by agent.delegate() when the held grant has no consenting user or is not delegable, the credential’s operator ceiling is off (a managed secret’s delegation policy or the OAuth provider’s agent sharing setting), or delegable: true asks for a child that the depth ceiling leaves no further hop (HTTP 403 grant_not_delegable). detailReason names which: no_consenting_user, holder_not_delegable, ceiling_off, no_onward_hop or not_requested (null when the backend sent none).
  • CredentialRevokedError
  • AmbiguousGrantError — multiple grants match the identity + provider tuple. Pass account or use direct grantId mode.
  • NoDelegatedGrantError — the calling agent has no delegation for the resolved user on this provider. Recoverable via createConnectSessionForError().
  • PolicyViolationError — backend policy denial.
  • StepUpRequiredError — extends PolicyViolationError. A content_match step-up obligation requires a fresher user session.
  • RedactDischargeFailedError — extends PolicyViolationError. The request was denied because a redact obligation could not be applied safely.
  • InsufficientScopeError — API key lacks the required scope.
  • TokenRefreshInProgressError — concurrent refresh in flight; retry shortly.
  • QuotaExceededError — a request-quota policy window is exhausted. Exposes retryAfter (seconds until the window resets, capped at 2,678,400 seconds / 31 days — a quota window is a calendar period up to a month long, and the response reports the longest exhausted one; may be undefined). A policy denial, not a transient outage — the SDK never auto-retries it.
  • RateLimitError — the platform rate limit for the API key (or the organization-wide aggregate, when configured) is exhausted. A transient throttle, not a denial: wait retryAfter seconds and retry the same request (retryAfter is capped at 3,600 seconds — one hour, above the platform’s legitimate retry guidance). Also exposes scope (which ceiling was hit — a plain string, not a closed union, so a ceiling added later stays parseable; the known values are exported as RATE_LIMIT_SCOPE_VALUES, currently "ip", "api_key" and "organization", ordered outermost-first), limit, and windowSeconds. Each of those three may be undefined — not every response carrying this error code supplies them, so branch on a missing value rather than assuming one. Also thrown for a non-JSON 429 from an intermediary such as an edge provider: the limiting layer is unknown, so scope, limit and windowSeconds are undefined, retryAfter is set only from a Retry-After header, and details is { status_code: 429 }.
  • ProviderAPIError — provider returned a 4xx/5xx that is not a scope or credential-rejection failure.
  • ScopeReauthRequiredError — provider returned 403 insufficient_scope, or the backend already knew the grant’s scopes are drifted. Exposes missingScopes and providerId.
  • ProviderUnauthorizedError — provider returned 401: the credential was revoked, expired, or otherwise invalidated provider-side. Exposes grantId, providerId, statusCode, responseBody. Recovery: re-authorize an OAuth grant via a new Connect session, or update the stored managed secret. A 401 whose WWW-Authenticate challenge carries error="insufficient_scope" (RFC 6750 — the credential is still valid, it just lacks a scope) surfaces as a generic ProviderAPIError instead.
  • ConnectFlowError — generic flow failure, including an unavailable/expired token on the first poll, an unknown terminal state, or a total usage-limit application failure.
  • ConnectDeniedError — user clicked Deny.
  • ConnectConfigError — provider configuration issue (invalid redirect URI, unknown client, etc.), including provider_configuration_unavailable, shared_dev_credential_unavailable, and managed_oauth_scope_approval_required when fresh authorization cannot start. createManagedSecretConnectSession also throws it with details.error === "idp_signin_not_configured" (HTTP 409, details.retryable === false) when the application’s identity provider cannot sign users in to approve the delegation. details.details.reason says what to change: missing_client_id or missing_client_secret — add the sign-in client ID and secret to the identity provider; issuer_cannot_sign_in — use an issuer that supports OpenID Connect sign-in (for WorkOS, the AuthKit issuer https://<slug>.authkit.app). It also throws it with details.error === "no_eligible_grants" (HTTP 409) when the user holds no credential the delegation could use; details.details.reason is no_grant, grant_expired, delegation_depth_exceeded, group_source_not_allowed or agent_not_allowlisted (see the reasons). Only a key bound to no managed agent receives no_eligible_grants: an app key, including an Agent from App.getAgent(), or an agent-typed alter_ak_… key minted under API Keys. A managed agent’s own key never does.
  • ConnectTimeoutError — Alter received no completion callback before the local deadline or before a session observed pending expired. details.reason is "poll_deadline_elapsed" or "session_expired"; without a callback, an exact provider-side error is unknowable. A user who signed in to the provider but closed Connect without confirming also ends here; when the Alter deployment holds new connections until Confirm, no connection from that session is usable.
  • ApprovalError — base class.
  • ApprovalDeniedError
  • ApprovalExpiredError
  • ApprovalTimeoutError — local wait elapsed before any decision. Original transient (when present) is preserved on .cause.
  • ApprovalExecutionFailedError
  • AgentError — base class.
  • InvalidKeyError
  • KeyExpiredError
  • KeyRotationExpiredError
  • KeyInactiveError
  • KeyAppNotFoundError
  • AgentNotFoundError
  • CallerAgentMismatchError
  • CallerAgentResolutionError
  • AgentNameExistsError
  • AgentConcurrentUpdateError — the agent changed after it was read. Fetch it again, recompute the full desired scope allowlist, and retry with the fresh version.
  • AgentInactiveError
  • AgentRevokedError
  • MeRequiresAgentKeyError — agent.me() was called through an impersonating Agent returned by App.getAgent(), so the request still used an application key rather than a genuine agent key.
  • KeyRevokedError
  • KeyAlreadyRevokedError
  • KeyNotFoundError
  • LastActiveKeyError — would revoke the agent’s only remaining active key. Pass force: true to override.
  • AgentKeyLimitError — the mint would take the agent past its per-agent active-key cap. Revoke a key it no longer uses, then retry.
  • AgentCannotMintSubagentsError
  • IdempotencyKeyBodyMismatchError
  • IdempotencyKeyAgentRevokedError
  • IdempotencyKeyAgentInactiveError
  • NetworkError — TCP / DNS / socket failure.
  • TimeoutError — extends NetworkError. Local or remote timeout.

Every AlterSDKError carries details: Record<string, unknown>. toString() returns only the safe message and never stringifies details. Common detail keys:

KeyTypeDescription
status_codenumberHTTP status code from the backend or provider.
grant_idstringGrant the call resolved to (when applicable).
provider_idstringProvider slug (when applicable).
method, urlstringHTTP method and pre-injection URL.
reasonstringBackend-reported reason code.
retry_afternumberSeconds to wait before retrying — a quota window reset or a rate-limit backoff. QuotaExceededError and RateLimitError also expose it as the typed retryAfter field (may be undefined). On both, the typed field prefers the Retry-After response header over this body value and caps the result — at 3,600 seconds for RateLimitError, at 2,678,400 (31 days) for QuotaExceededError, whose windows are calendar periods — so the two can differ.

The following public fields supplement message, name, and details:

ClassFields
GrantExpiredErrorproviderId, agentId, appUserId (string | undefined)
GrantRevokedErrorgrantId: string | undefined
CredentialRevokedErrorgrantId, providerId, appUserId (string | undefined)
GrantNotFoundErrorproviderId, agentId, appUserId (string | undefined)
AmbiguousGrantErrorproviderId, accountIdentifiers, accountWasProvided, appUserIds, grantIds, candidates, candidatesTruncated — when candidatesTruncated is true every disambiguator here is a bounded sample, so an absence from any of them proves nothing
NoDelegatedGrantErrorproviderId, agentId, appUserId (string | undefined)
PolicyViolationErrorpolicyError: string | undefined
StepUpRequiredErrormaxSessionAgeSeconds: number | undefined; inherited policyError is "step_up_required"
RedactDischargeFailedErrorno extra fields; inherited policyError is "redact_discharge_failed"
SiblingLabelConflictErrorlabel: string | null
InsufficientScopeErrorrequired, granted, missing, scopeVersion, currentScopeVersion, scopeVersionMismatch, documentationUrl; aliases requiredScopes, grantedScopes, docsUrl
TokenRefreshInProgressErrorgrantId: string | undefined
QuotaExceededErrorretryAfter: number | undefined
ProviderAPIErrorstatusCode: number | undefined, responseBody: string | undefined
ScopeReauthRequiredErrorgrantId, providerId, missingScopes; inherits provider response fields
ProviderUnauthorizedErrorgrantId, providerId; inherits provider response fields
ApprovalError and subclassesapprovalId: string | null
AgentError and subclassescode: string, hint?: string

AmbiguousGrantError.candidates contains GrantCandidate entries with grantId, label, accountIdentifier, and accountDisplayName. The account fields are nullable.

Recover deliberately, never by taking the first candidate:

try {
response = await app.request(HttpMethod.GET, url, { provider: "<provider-id>", userToken: userJwt });
} catch (e) {
if (!(e instanceof AmbiguousGrantError)) throw e;
const grantId = chooseGrant(e.candidates); // the user's choice, persisted on their profile
response = await app.request(HttpMethod.GET, url, { grantId, userToken: userJwt });
}

For the typed exception fields and recovery flows for each code, see Errors.

Version compatibility: BlockedDestinationError, and RateLimitError for a non-JSON 429, require an @alter-ai/alter-sdk release after 0.27.0. Version 0.27.0 and earlier throw a generic BackendError for both responses.

BlockedDestinationError inherits from BackendError. The requested destination failed the provider’s host/path allowlist or the network security check. When a call held for approval is refused as the approved call executes, the approval wait raises ApprovalExecutionFailedError instead. Correct the URL before retrying. Its details includes error: "blocked_destination", status_code, and request_id when the server supplied one; use the request ID when investigating the refusal. Private resolver addresses and credentials are not part of the diagnostic contract.

Report an issue with this page

Necessary

Required for sign-in, security, authorization, and remembering your choices.

Always active

Analytics

Helps us understand which product and documentation features are useful.

Performance diagnostics

Uses performance tracing and privacy-masked session replay to diagnose problems.

You can change these choices at any time from Cookie settings.