SDK error handling
Every Driftstack SDK ships a typed error hierarchy mapping
application/problem+json responses (RFC 9457) to language-native
exceptions. Catch by category for control-flow logic; catch the
base type for blanket logging.
The hierarchy is consistent across TypeScript / Python / Go — the same problem-type URIs map to matching errors in every SDK. The table below lists the class name in each language (a couple of names differ per language — see the naming note).
Hierarchy
Server problem-type URIs live under the stable
https://errors.driftstack.dev/<slug> host. Dispatch on the slug,
not on HTTP status.
| Server problem-type slug | TS class | Python exception | Go type | Retryable |
|---|---|---|---|---|
unauthorized |
AuthError |
AuthError |
*AuthError (ErrAuth) |
no |
invalid-key |
InvalidKeyError |
InvalidKeyError |
*InvalidKeyError (ErrInvalidKey, also ErrAuth) |
no |
expired-key |
ExpiredKeyError |
ExpiredKeyError |
*ExpiredKeyError (ErrExpiredKey, also ErrAuth) |
no |
revoked-key |
RevokedKeyError |
RevokedKeyError |
*RevokedKeyError (ErrRevokedKey, also ErrAuth) |
no |
forbidden |
ForbiddenError |
ForbiddenError |
*ForbiddenError |
no |
dashboard-only |
DashboardOnlyError |
DashboardOnlyError |
*DashboardOnlyError |
no |
validation-failed |
ValidationError |
ValidationError |
*ValidationError |
no |
not-found |
NotFoundError |
NotFoundError |
*NotFoundError |
no |
conflict |
ConflictError |
ConflictError |
*ConflictError |
no |
rate-limited |
RateLimitError |
RateLimitError |
*RateLimitError |
yes |
concurrency-limit |
ConcurrencyLimitError |
ConcurrencyLimitError |
*ConcurrencyLimitError |
no |
tier-limit |
TierLimitError |
QuotaExceededError |
*QuotaExceededError |
no |
driver-not-integrated |
DriverNotIntegratedError |
DriverError |
*DriverError |
no |
session-timeout |
SessionTimeoutError |
SessionTimeoutError |
*SessionTimeoutError |
no |
session-destroyed |
SessionDestroyedError |
SessionDestroyedError |
*SessionDestroyedError |
no |
maintenance |
MaintenanceError |
MaintenanceError |
*MaintenanceError (ErrMaintenance) |
yes |
internal |
InternalError |
InternalError |
*InternalError (ErrInternal) |
yes |
| transport (network / parse / timeout) | TransportError |
TransportError |
*TransportError |
yes |
concurrency-limit has two meanings. At create it is the open-session cap
(current_sessions, limit). On an agent-session message with
ai_tasks_in_flight: true it is the limit of 3 AI tasks running at once
(which of rate-limited and this answer you get depends on how your account’s
AI is billed): wait, then send the message again. Do not close sessions for
it. TypeScript has the flag as err.aiTasksInFlight; Python and Go read it
from the problem body: e.problem.get("ai_tasks_in_flight") and
cle.Problem["ai_tasks_in_flight"] == true. Read it that way: every other
answer has no such key.
The sign-in and account-action types (legal-acceptance-required,
mfa-step-up-required, invalid-credentials and the rest) are raised only in
the dashboard, so a program using an API key does not receive them; their SDK
classes (LegalAcceptanceRequiredError, MfaStepUpRequiredError and the rest)
remain, marked deprecated. One still reaches a program: the OAuth token
exchange answers legal-acceptance-required while the account that approved
the app owes a legal acceptance (see OAuth). The
error reference lists them.
All extend DriftstackError in TypeScript and Python. Go exports
no base error type — every typed error embeds an unexported base
struct, so match a category with errors.Is against the exported
sentinels (ErrAuth, ErrTransport, …) or pull the concrete type
with errors.As, as the Go example below shows.
Catching the auth category: the languages differ. In Python
InvalidKeyError/ExpiredKeyError/RevokedKeyErrorsubclassAuthError, soexcept AuthErrorcatches all four. In TypeScript they extendDriftstackErrordirectly — aninstanceof AuthErrorcheck matches ONLY theunauthorizedtype, so check the key-specific classes explicitly. In Go,errors.Is(err, ErrAuth)matches the whole category;errors.As(err, &auth *AuthError)does not match the key-specific types.
Naming note: for the
tier-limitproblem type the TypeScript SDK exportsTierLimitError(the original name, shipped since 0.1.x), whereas the Python and Go SDKs name itQuotaExceededError. All three surface the same payload fields (current/limit/recordType, snake_case in Python).
Agent-session (AI) errors
Agent sessions add these typed errors to the table above:
| Server problem-type slug | TS class | Python exception | Go type | Retryable |
|---|---|---|---|---|
ai-credits-exhausted |
AiCreditsExhaustedError |
AiCreditsExhaustedError |
*AiCreditsExhaustedError |
no |
bundled-llm-budget-exhausted |
BundledLlmBudgetExhaustedError |
BundledLlmBudgetExhaustedError |
*BundledLlmBudgetExhaustedError |
no |
bundled-llm-consent-required |
BundledLlmConsentRequiredError |
BundledLlmConsentRequiredError |
*BundledLlmConsentRequiredError |
no |
byok-anthropic-required |
ByokAnthropicRequiredError |
ByokAnthropicRequiredError |
*ByokAnthropicRequiredError |
no |
device-unavailable |
DeviceUnavailableError |
DeviceUnavailableError |
*DeviceUnavailableError |
no |
feature-unavailable |
FeatureUnavailableError |
FeatureUnavailableError |
*FeatureUnavailableError |
no |
payload-too-large |
PayloadTooLargeError |
PayloadTooLargeError |
*PayloadTooLargeError |
no |
profile-in-use |
ProfileInUseError |
ProfileInUseError |
*ProfileInUseError |
no |
proxy-required |
ProxyRequiredError |
ProxyRequiredError |
*ProxyRequiredError |
no |
proxy-validation-failed |
ProxyValidationFailedError |
ProxyValidationFailedError |
*ProxyValidationFailedError |
no |
session-not-ready |
SessionNotReadyError |
SessionNotReadyError |
*SessionNotReadyError |
yes |
storage-quota-exceeded |
StorageQuotaExceededError |
StorageQuotaExceededError |
*StorageQuotaExceededError |
no |
pair-mode-conflict |
PairModeConflictError |
PairModeConflictError |
*PairModeConflictError |
no |
pair-mode-invalid-transition |
PairModeStateInvalidTransitionError |
PairModeStateInvalidTransitionError |
*PairModeStateInvalidTransitionError |
no |
Several agent-session refusals reuse the general classes and carry extra
fields: a ConflictError can carry turn_in_progress, session_status or
idempotency_status, and a ForbiddenError can carry requires_own_key and
model. Read them from err.extensions (TypeScript), err.problem
(Python) or Problem (Go). In Python, ForbiddenError subclasses
AuthError, so catch ForbiddenError before AuthError if you treat them
differently.
message() is never retried for you, whatever the retry predicate says, and
with an Idempotency-Key an error response is final for that key. See
Run AI tasks from your code
for what to do about each one, and Idempotency keys
for the requests that accept a key.
TypeScript
import { randomUUID } from 'node:crypto';
import {
Driftstack,
DriftstackError,
AuthError,
InvalidKeyError,
ExpiredKeyError,
RevokedKeyError,
RateLimitError,
ConcurrencyLimitError,
TierLimitError,
ValidationError,
} from '@driftstack/sdk';
const client = new Driftstack({ apiKey: process.env.DRIFTSTACK_API_KEY! });
try {
const session = await client.agentSessions.create(
{ mode: 'ai', proxy_id: process.env.DRIFTSTACK_PROXY_ID! },
{ idempotencyKey: randomUUID() },
);
// ...
} catch (err) {
if (
err instanceof AuthError || // unauthorized
err instanceof InvalidKeyError ||
err instanceof ExpiredKeyError ||
err instanceof RevokedKeyError
) {
// re-mint key + retry, or surface to ops. NOTE: in TS the three
// bad-key classes do NOT extend AuthError — check them explicitly.
} else if (err instanceof ConcurrencyLimitError) {
// At create: too many open sessions. On a message, err.aiTasksInFlight === true
// means 3 AI tasks are running: wait, send the message again, close nothing.
console.warn(
`at the limit: ${err.currentSessions}/${err.limit} (sessions, or AI tasks on a message)`,
);
} else if (err instanceof TierLimitError) {
console.warn(`quota exceeded for ${err.recordType}: ${err.current}/${err.limit}`);
} else if (err instanceof RateLimitError) {
await sleep((err.retryAfterSeconds ?? 1) * 1000);
// retry
} else if (err instanceof ValidationError) {
console.error('bad request:', err.message);
} else if (err instanceof DriftstackError) {
// catch-all for anything typed Driftstack
} else {
throw err; // not a Driftstack error
}
}
The default retry policy (3 retries, exponential backoff with full
jitter, honours Retry-After) handles TransportError,
RateLimitError, and InternalError (5xx internal)
automatically. Other typed errors propagate
immediately so your code can route them.
Python
import os
import uuid
from driftstack import (
Driftstack,
AuthError,
ConcurrencyLimitError,
QuotaExceededError,
RateLimitError,
ValidationError,
DriftstackError,
)
client = Driftstack(api_key="ds_live_…")
try:
session = client.agent_sessions.create(
{"mode": "ai", "proxy_id": os.environ["DRIFTSTACK_PROXY_ID"]},
idempotency_key=str(uuid.uuid4()),
)
except AuthError:
...
except ConcurrencyLimitError as e:
# At create: too many open sessions. On a message, e.problem.get("ai_tasks_in_flight")
# is True when 3 AI tasks are running: wait, send the message again, close nothing.
print(f"at the limit: {e.current_sessions}/{e.limit} (sessions, or AI tasks on a message)")
except QuotaExceededError as e:
print(f"quota exceeded for {e.record_type}: {e.current}/{e.limit}")
except RateLimitError as e:
time.sleep(e.retry_after_seconds or 1)
except ValidationError as e:
print(f"bad request: {e.message}")
except DriftstackError:
raise # catch-all for anything else typed Driftstack
Go
import (
"errors"
"time"
driftstack "github.com/driftstackdev/driftstack-go"
)
client := driftstack.New(os.Getenv("DRIFTSTACK_API_KEY"))
defer client.Close()
session, err := client.AgentSessions.Create(ctx,
&driftstack.CreateAgentSessionRequest{Mode: "ai", ProxyID: os.Getenv("DRIFTSTACK_PROXY_ID")},
&driftstack.CreateOptions{IdempotencyKey: "create-" + time.Now().Format(time.RFC3339Nano)})
if err != nil {
var auth *driftstack.AuthError
if errors.As(err, &auth) {
// matches ONLY the `unauthorized` problem type — for the whole
// auth category (incl. invalid/expired/revoked key) use the
// errors.Is(err, driftstack.ErrAuth) sentinel below.
}
var rl *driftstack.RateLimitError
if errors.As(err, &rl) {
time.Sleep(time.Duration(rl.RetryAfterSeconds) * time.Second)
// retry
}
var cle *driftstack.ConcurrencyLimitError
if errors.As(err, &cle) {
// At create: too many open sessions. On a message, cle.Problem["ai_tasks_in_flight"] == true
// means 3 AI tasks are running: wait, send the message again, close nothing.
log.Printf("at the limit: %d/%d (sessions, or AI tasks on a message)", cle.CurrentSessions, cle.Limit)
}
if errors.Is(err, driftstack.ErrAuth) {
// sentinel-style match for AuthError category
}
log.Fatal(err)
}
errors.As for the structured payload, errors.Is for category
matching against the package-level sentinels (ErrAuth,
ErrTransport, etc.).
Retry policy details
Retryable errors by default:
TransportError(network / timeout / response parse failure).RateLimitError(HTTP 429). Honours the server’sretry-after-secondsvalue when present.InternalError(the 5xxinternalproblem type).
Non-retryable: every other typed error. Auth errors aren’t retried
because retrying with the same bad key won’t help. Validation
errors aren’t retried because the client request is wrong.
Concurrency / quota errors aren’t retried because retrying without
freeing capacity will keep failing. The other 5xx-status typed
errors (DriverError 502, DriverNotIntegratedError 503,
SessionTimeoutError 504) are treated as terminal and are NOT
auto-retried.
Override via constructor options:
TypeScript:
const client = new Driftstack({
apiKey,
retry: {
maxAttempts: 5,
initialDelayMs: 500,
maxDelayMs: 10_000,
// Backoff multiplier is fixed at 2× in the TS SDK (it is not a config
// field; Python and Go expose it as backoff_multiplier / BackoffMultiplier).
},
});
Python:
from driftstack.retry import RetryConfig
client = Driftstack(
api_key="ds_live_…",
retry=RetryConfig(max_retries=5, initial_delay_ms=500, max_delay_ms=10_000),
)
# disable entirely (predictable testing):
client = Driftstack(api_key="…", retry=RetryConfig(enabled=False))
Go:
client := driftstack.New(
apiKey,
driftstack.WithRetry(driftstack.RetryConfig{
MaxRetries: 5,
InitialDelay: 500 * time.Millisecond,
MaxDelay: 10 * time.Second,
BackoffMultiplier: 2.0,
}),
)
// disable entirely:
client = driftstack.New(apiKey, driftstack.WithRetry(driftstack.RetryConfig{Disabled: true}))
Cancellation
All three SDKs honour cancellation:
- TypeScript: pass an
AbortSignalvia per-call options ({ signal }). Aborting between retry attempts terminates the retry loop; aborting mid-request cancels the in-flightfetch. - Python: the sync client uses thread-blocking sleeps; the
async client uses
asyncio.sleepand respectsasyncio.CancelledError. - Go: pass a
context.Contextfirst arg to every method. Cancelling the context aborts both the retry loop + the in-flighthttp.Request.
Surfacing errors to your end users
The message field on every DriftstackError is human-readable
but technical. For customer-facing surfaces, map the error to a
user-friendly message:
function userMessageFor(err: unknown): string {
if (err instanceof AuthError) return 'Your session expired — please sign in again.';
if (err instanceof ConcurrencyLimitError) {
if (err.aiTasksInFlight === true) {
return `${err.limit} AI tasks are already running. Yours starts when one of them finishes: nothing needs to be stopped.`;
}
return `You're at the maximum number of concurrent sessions (${err.limit}). Stop one you started before starting a new one.`;
}
if (err instanceof RateLimitError) return "We're going too fast — give it a moment.";
if (err instanceof DriftstackError) return 'Something went wrong on our end.';
return 'Unexpected error.';
}
Don’t expose raw DriftstackError.message in customer UIs.
See also
- SDK installation — install + configure each SDK.
- Quickstart — first session in five minutes.
- Webhook events — server-pushed events use a separate signature-verification path; not error-handling.