Skip to content
2DAITeam edited this page Oct 13, 2026 · 11 revisions

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/5xx with 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.

Clone this wiki locally