Errors - 2DAICommunity/2dai-cloud-sdk GitHub Wiki
Error Handling
Every failed request throws an ApiError (or a subclass). Branch on the concrete
class or on err.code. All carry .code, .httpStatus, and .details.
import {
InsufficientCreditError,
SpendLimitError,
NsfwRejectedError,
ScopeError,
ApiError,
} from '2dai-cloud-sdk';
try {
await client.generate.image({ prompt: '…' });
} catch (err) {
if (err instanceof InsufficientCreditError) {
console.log('Top up — short by', err.deficitUsd);
} else if (err instanceof SpendLimitError) {
console.log('Key cap reached at', err.spendLimitUsd);
} else if (err instanceof NsfwRejectedError) {
console.log('Blocked by moderation');
} else if (err instanceof ScopeError) {
console.log('Key is missing scope:', err.requiredScope);
} else if (err instanceof ApiError) {
console.log(err.code, err.httpStatus);
} else {
throw err;
}
}
Reference
| Class | HTTP | Codes | Extra fields |
|---|---|---|---|
AuthError |
401 | INVALID_API_KEY, ACCOUNT_UNAVAILABLE, any code-less 401 (e.g. CDN gate) |
— |
ScopeError |
403 | MISSING_SCOPE — requiredScope is 'finance' on the wallet reads (an opt-in scope your key probably doesn't carry) |
requiredScope, scopes |
InsufficientCreditError |
402 | INSUFFICIENT_CREDIT |
costUsd, creditUsd, deficitUsd |
SpendLimitError |
402 | KEY_SPEND_LIMIT_REACHED |
costUsd, spentUsd, spendLimitUsd |
TierError |
403 | *_NOT_ALLOWED_FOR_TIER, INSUFFICIENT_TIER |
requiredTier, requiredTierName (the first tier that opens it), tool (TOOL_NOT_ALLOWED_FOR_TIER: video, video-next, image-next, …) or quality; message is a ready sentence (SDK ≥ 2.8.0) |
NsfwRejectedError |
403 | NSFW_REJECTED, NSFW_REFS_BLOCKED |
nsfwRate (rejected) / blockedCreationIds (refs) |
QueueLimitError |
409 | QUEUE_LIMIT_REACHED |
maxPending, current |
NotFoundError |
404 | CREATION_NOT_FOUND, QUEUE_ITEM_NOT_FOUND, NOT_FOUND, KEY_NOT_FOUND, any code-less 404 |
— |
RateLimitError |
429 | (HTTP 429) | — |
ValidationError |
400/409/410/422 | other 4xx — INVALID_PROMPT (empty prompt), PROMPT_TOO_LONG (over the account's promptMaxChars; carries max/got/tier/next), NO_FILE, INVALID_MEDIA (an upload the platform cannot read), PROTECTED_MEDIA (a DRM-protected upload — iTunes / FairPlay, CENC), TOO_MANY_REFS (a tool's strict ref cap; carries max/got), the listing rejections CONFLICTING_FILTERS (two modes in one call) and RANDOM_NOT_SUPPORTED (random on the trash), NOT_FLAGGED (explainNsfw on a clean row), SCORE_PARSE_FAILED (422, ref scoring), NOT_PENDING (409, queue.cancel after a worker picked the job up — current status in details.status), the similar-rejections REGEN_UNSUPPORTED_TOOL (raw upload / wallpaper-resize), REGEN_MISSING_SOURCE/REGEN_MISSING_PROMPT and REGEN_REFS_UNAVAILABLE (422, every identity reference of the row is unusable), the clone rejections CLONE_UNSUPPORTED_MEDIA (video/gif) and CREATION_HAS_NO_MEDIA, and the drive 409s: NOT_IN_TRASH (delete before trash), CREATION_IN_TRASH (publish while trashed), UPLOADED_NOT_PUBLISHABLE, DUPLICATE_IN_FOLDER (same pixels, same folder), FOLDER_LIMIT_REACHED / GROUP_LIMIT_REACHED (caps: 500 folders, 100 groups) |
— |
GenerationFailedError |
— | any non-completed terminal (failed/cancelled/dismissed/timeout/expired) |
status, queueId, errorCode, errorMessage (SDK ≥ 2.5.6 — see below) |
TimeoutError |
— | client-side wait deadline | details.queueId (no typed getter) |
ApiError |
any | anything else — notably KEY_POLICY_CHANNEL (403, the key's modelChannels policy does not include the videoModel / imageModel channel asked; channel), INVALID_FOLDER (403, upload targetFolderId you can't write to), PRICE_UNAVAILABLE (503, no cached token quote yet — retry later), and RANDOM_NOT_SUPPORTED thrown client-side (httpStatus: 0) when random is passed to list instead of using creations.random() |
— |
Why a generation failed
A content-rated result (nsfwFlagged: true, nsfwRate 0.8 or 0.99) is not a failure: the job
completes and is charged, the creation is kept but masked in the owner's drive and cannot be
published — see Content rating. Only NSFW_MAX_EXCEEDED below is a rejection.
A job that ends failed, timeout or expired carries two fields (SDK ≥ 2.5.6),
on the QueueState from queue.get / queue.waitFor and on the
GenerationFailedError a blocking generate.* throws:
errorCode— a stable machine code: branch on this.errorMessage— a readable sentence: show this to a person as is.
error keeps the raw outcome string, unchanged, for older code. A job you
cancelled carries neither field.
errorCode |
What happened | errorMessage (example) |
|---|---|---|
CAPACITY_FULL |
No render capacity was left for this job | No capacity left right now. You weren't charged — try again soon. |
EXPIRED |
The job waited its whole queue time without free capacity | No capacity was free in time. You weren't charged — try again soon. |
GENERATION_TIMEOUT / PROCESSING_TIMEOUT |
The render took too long and was stopped | Took too long and was stopped. You weren't charged — try a lower quality (or shorter clip). |
NSFW_MAX_EXCEEDED |
The result was filtered out (your Block NSFW setting, or the platform ceiling) | The result was filtered out by your Block NSFW setting. You weren't charged — … |
QUALITY_UNAVAILABLE / VIDEO_MODEL_UNAVAILABLE / IMAGE_MODEL_UNAVAILABLE |
That quality or model channel isn't available right now | Max quality isn't available right now. You weren't charged. |
TIER_DOWNGRADED |
Your tier no longer includes this quality or length | Your tier no longer includes this quality or length. You weren't charged — pick another option. |
REFERENCE_FETCH_FAILED |
The reference images could not be loaded | The reference images could not be loaded. You weren't charged — try again in a moment. |
GENERATION_FAILED |
Anything else | Generation failed on our side. You weren't charged — please try again. |
Every one of these outcomes is refunded. CAPACITY_FULL and EXPIRED are
worth a retry a little later:
try {
await client.generate.image({ prompt: 'a lighthouse at dusk' });
} catch (e) {
if (e instanceof GenerationFailedError) {
console.log(e.errorMessage ?? e.message);
if (e.errorCode === 'CAPACITY_FULL' || e.errorCode === 'EXPIRED') scheduleRetry();
} else throw e;
}
Catching a specific case
Every server code rides a typed class, so a narrow catch stays readable:
try {
await client.creations.delete(c);
} catch (e) {
if (e instanceof ValidationError && e.code === 'NOT_IN_TRASH') {
await client.creations.trash(c);
await client.creations.delete(c);
} else throw e;
}
Retries
- Idempotent GETs are retried automatically on
429/5xxwith exponential backoff (maxRetries, default 2). - Generation submits are never auto-retried — they may charge. Your retry
of an identical call within a few seconds is de-duplicated server-side. Pass
your own hex
clientToken([0-9a-f]{1,64}) to control the window.