Errors
Every failure in the SDK is a thrown EdgibleError subclass. There are no error-shaped return values, no null fallbacks, and no need to parse message text: each error carries a stable machine-readable code, the backend requestId for support, and an honest retryable flag the SDK’s own retry layer already acts on.
The hierarchy
Section titled “The hierarchy”All classes extend EdgibleError and are exported from @edgible-team/sdk.
| Class | Fires on | Default code | retryable |
|---|---|---|---|
ValidationError | 400 (and unexpected 4xx) | VALIDATION_FAILED | no |
AuthenticationError | 401; missing/expired credentials | AUTH_INVALID_CREDENTIALS, AUTH_NO_CREDENTIALS, AUTH_TOKEN_EXPIRED | no |
AuthorizationError | 403 (authenticated, not permitted) | FORBIDDEN | no |
NotFoundError | 404 | NOT_FOUND | no |
ConflictError | 409 (already exists, wrong state for the operation) | CONFLICT | no |
RateLimitError | 429 | RATE_LIMITED | yes |
ServerError | 5xx | SERVER_ERROR | yes |
NetworkError | transport failure (DNS, connection reset, …) | NETWORK_ERROR | yes |
TimeoutError | request aborted / deadline exceeded | REQUEST_TIMEOUT | yes |
ConfigurationError | client-side misconfig (bad env, empty baseUrl, unsupported credential) | CONFIG_INVALID, CONFIG_MISSING_BASE_URL, SERVICE_ACCOUNT_NOT_SUPPORTED | no |
WaiterError | a long-running operation failed or timed out while being waited on | WAITER_FAILED, WAITER_TIMEOUT | no |
Fields on every error
Section titled “Fields on every error”class EdgibleError extends Error { readonly code: EdgibleErrorCode; // stable, machine-readable — branch on this readonly category: ErrorCategory; // coarse class: 'validation' | 'not_found' | 'rate_limit' | … readonly statusCode: number | undefined; // HTTP status, when the error came from a response readonly requestId: string | undefined; // backend trace id — include in bug reports readonly retryable: boolean; // could a retry succeed? readonly details: unknown; // structured context (e.g. validation field errors) readonly cause: unknown; // the original error, preserved}Two subclasses add a field:
RateLimitError.retryAfterMs— parsed from the response’sRetry-Afterheader when present.WaiterError.lastState— the last observed operation state ({ phase, terminal, raw }).
A note on code: when the backend supplies a code in the error body, it passes through verbatim; otherwise the SDK assigns the default for the status (table above). EdgibleErrorCode is an open string union — the known set autocompletes, but always be prepared for resource-specific codes. Branch on code or instanceof, never on message — messages may change between releases; codes do not.
Branching on errors
Section titled “Branching on errors”Use instanceof for the coarse class, code when you need finer grain:
import { EdgibleError, NotFoundError, ValidationError,} from '@edgible-team/sdk';
try { await client.applications.delete(appId);} catch (err) { if (err instanceof NotFoundError) { // Already gone — fine for a delete. return; } if (err instanceof ValidationError) { // Bad input; structured context (e.g. field errors) is on `details`. console.error('Invalid request:', err.message, err.details); return; } if (err instanceof EdgibleError && err.retryable) { // ServerError / NetworkError / TimeoutError / RateLimitError that the // SDK already retried (see below) and still failed. Queue it, alert, // or surface to the user — don't tight-loop another retry. console.error(`Transient failure [${err.code}], requestId=${err.requestId}`); throw err; } throw err; // anything else is a real bug or a permission problem}When reporting a problem, always include err.requestId — it is the backend trace ID and lets operators find the exact request in logs.
What the SDK retries for you
Section titled “What the SDK retries for you”A retry layer is built into every client, so do not wrap SDK calls in your own retry loop for transient failures — you would multiply the backoff.
- What is retried: only errors with
retryable: true—ServerError(5xx),NetworkError,TimeoutError, andRateLimitError(429). All other errors fail fast on the first attempt. - How: up to 4 attempts total, exponential backoff with full jitter (random delay between 0 and the current backoff ceiling), base delay 200 ms, capped at 10 s per wait.
- 429s: when the response carries a
Retry-Afterheader, that delay is honoured (capped at the max delay) instead of the jittered backoff.
If a retryable error reaches your code, the SDK has already exhausted its attempts. Tune the policy per client:
const client = createClient({ retry: { maxAttempts: 6, baseDelayMs: 500, maxDelayMs: 30_000 },});One related behaviour lives outside the retry policy: on a 401, the auth layer performs exactly one token refresh and replays the request once before surfacing an AuthenticationError. See Authentication & credentials.
WaiterError (long-running operations)
Section titled “WaiterError (long-running operations)”Helpers that wait for an operation to finish (deploys, provisioning, migrations) throw WaiterError when the operation fails or doesn’t reach the expected state in time. It is never retried automatically, and it carries the last state the waiter observed:
catch (err) { if (err instanceof WaiterError) { console.error(`Stuck in phase '${err.lastState.phase}'`, err.lastState.raw); }}See Long-running operations for waiters, timeouts, and progress callbacks.