NodaroAccount
Nodaro Docs
DocumentationNode ReferenceModelsTemplatesAI Agents (MCP)DevelopersSelf-hostingResearchCompare
TypeScript SDK

Errors

Every error the Nodaro TypeScript SDK throws, with its HTTP status, code and fields, and what to do about credits, rate limits, conflicts and failed jobs.

Every error the Nodaro SDK throws for an API answer is an instance of NodaroError or one of its subclasses. A request that fails with an error status throws the subclass that matches the status. The run-and-wait helpers throw their own subclasses when a job fails, times out or is stopped. Catch the specific classes first and NodaroError last.

Every error class

ClassStatuscodeExtra fieldsThrown when
NodaroErrorAnyThe server's codeThe base class, and any error without a more specific class
UnauthorizedError401unauthorizedThe token is missing, expired or invalid
ForbiddenError403The server's code, else forbiddenmissingScope?Permission is denied, or an OAuth token lacks a scope
ModelUnavailableError403model_not_available or model_disabledThe model you chose is not offered, or an admin switched it off
ImportRefusedError403imports_blocked, import_source_disabled, link_imports_disabled or a rights_* codeAn import of a link was refused before anything was fetched
NotFoundError404The server's code, else not_foundThe item does not exist, or you cannot see it
RateLimitedError429The server's code, else rate_limitedToo many requests
InsufficientCreditsError402insufficient_credits, or another 402 coderequired?, available?The account cannot pay for the run
CreditCapExceededError402budget_exceeded, member_cap_exceeded or user_allowance_exceededrequired?, available?A spending cap someone else set refused the run
StorageExceededError413storage_exceededlimitBytes?, reason?What you sent does not fit: a full storage, or one file over the limit
WorkflowConflictError409workflow_conflict or production_busycurrentUpdatedAt?, currentVersion?, currentRecord?Someone else changed the item first
AlreadyRunningError409already_runningexecutionIdThe run you asked to start or continue is still running
AppRunVersionChangedError409app_run_version_changedrunVersion?, appVersion?An app run's Retry was refused because the app changed version
JobBlockedError422job_blockedThe deployment's content policy refused the request
StudioOpError4xxThe server's codeopIndexOne operation in a Studio batch was refused
JobFailedError0job_failedjobId, jobStatusA job you were waiting for failed or was cancelled
JobTimeoutError0job_timeoutjobId, timeoutMsA job did not finish within maxMs
JobAbortedError0job_abortedjobId?Your AbortSignal fired while waiting
JobHeldError0job_heldjobIdA job is held for human review
StudioPreviewUnavailable0studio_preview_unavailableThe deployment cannot preview a Studio batch
StudioPreviewAppliedError0studio_preview_appliedappliedA Studio batch you asked to preview was applied

Every class has four fields: message, a readable sentence; code, a stable string you can compare; status, the HTTP status; and details. A status of 0 means the error did not come from an HTTP answer. For example, JobTimeoutError is thrown by the SDK's own polling loop.

details holds every field of the error envelope other than code and message, as the server sent it. Examples are a trend refusal's slot, a publish refusal's reasons and a validation error's issues. It is {} when the envelope had nothing else, and for an error the SDK made itself.

try {
  await client.trends.run(slug, request)
} catch (err) {
  if (err instanceof NodaroError && err.code === "missing_slot") askFor(err.details.slot as string)
}

The constructor is new NodaroError(message, code, status, details?).

Some statuses map to one class, whatever code the server sent. A 403 becomes ForbiddenError, except a refused model, which becomes ModelUnavailableError, and a refused import of a link, which becomes ImportRefusedError. A 404 becomes NotFoundError, a 429 becomes RateLimitedError, and every 413 becomes StorageExceededError. ForbiddenError, NotFoundError and RateLimitedError keep the server's own code, such as node_not_available, continuation_not_found or rate_limit_exceeded. They fall back to forbidden, not_found and rate_limited only when the server sent no code. A 409 becomes WorkflowConflictError only with the code workflow_conflict or production_busy. A 409 already_running that names an executionId becomes AlreadyRunningError, and a 409 app_run_version_changed becomes AppRunVersionChangedError. Errors with any other status, such as 400 or 503, and a 409 with any other code, arrive as NodaroError with the server's own code.

Catch errors in order

import {
  ForbiddenError,
  InsufficientCreditsError,
  ModelUnavailableError,
  NodaroError,
  NotFoundError,
  RateLimitedError,
  StorageExceededError,
  UnauthorizedError,
} from "@nodaro/sdk"

try {
  await client.workflows.run(workflowId)
} catch (err) {
  if (err instanceof UnauthorizedError) {
    redirectToLogin()
  } else if (err instanceof ModelUnavailableError) {
    showError("This model is not available. Choose another model.")
  } else if (err instanceof ForbiddenError) {
    if (err.missingScope) requestAdditionalScopes([err.missingScope])
    else showError("You do not have permission to do this.")
  } else if (err instanceof InsufficientCreditsError) {
    showCreditPaywall({ required: err.required, available: err.available })
  } else if (err instanceof RateLimitedError) {
    await retryWithBackoff()
  } else if (err instanceof StorageExceededError) {
    if (err.reason === "file_too_large") showError("This file is too large.")
    else showError(`Storage limit of ${err.limitBytes} bytes reached.`)
  } else if (err instanceof NotFoundError) {
    showError("Not found.")
  } else if (err instanceof NodaroError) {
    console.error(`API error ${err.status} (${err.code}): ${err.message}`)
  } else {
    throw err // a network failure or a timeout, not an API answer
  }
}

A request that exceeds the client's timeoutMs, and a network failure, reject with the runtime's own error, such as an AbortError or a TypeError. These are not NodaroError instances.

Authentication and permission

UnauthorizedError

HTTP 401. The token is missing, has expired, or is invalid. Get a new token, or send the user through sign-in again, then retry.

ForbiddenError

HTTP 403. The caller may not do this. When an OAuth token was not granted a scope the endpoint needs, missingScope names that scope, for example workflows:execute. Ask the user to approve it, then retry with the new token. See Scopes and missing permissions.

Other reasons include an edition that does not offer the feature and a role that is too low. code is the server's own code, for example insufficient_scope or node_not_available, so you can branch on it. It is forbidden only when the server sent no code.

ModelUnavailableError

HTTP 403 that refuses the model, not the caller. Nodaro answers it before any credits are held. Choose another model: a retry, a top-up or a wider scope changes nothing.

codeRefusal
model_not_availableThis deployment does not offer the model. The model is outside its edition or surface, or its operator hid it.
model_disabledAn admin switched the model off.

ModelUnavailableError extends ForbiddenError, so an existing instanceof ForbiddenError check still catches it. Check instanceof ModelUnavailableError first to tell the two apart. Read code to show your own sentence, not the English message. MODEL_UNAVAILABLE_CODES, a ReadonlySet<ModelUnavailableCode>, lists the two codes. The error does not name the model: the refused model is the provider of your request, or one of its models for a request that runs several. A 403 with insufficient_scope, with any other code or with no code stays a plain ForbiddenError.

The constructor is new ModelUnavailableError(code, message?). The ForbiddenError constructor is new ForbiddenError(message?, missingScope?, code?).

ImportRefusedError

HTTP 403 that refuses an import of a link, before anything was fetched: media.downloadVideo(), uploads.importUrl() and the audio imports. A measure on the account, the content rights check or the deployment's site policy said no. See Content rights and Repeat infringers.

codeRefusal
imports_blockedImporting links is blocked on the account, or the account is closed, under the deployment's repeat-infringer policy. The message says until when: a date in UTC, or until an administrator lifts the block.
import_source_disabledThe deployment does not allow imports from the link's site. The message names no site.
link_imports_disabledThe deployment turned imports from links to other sites off. A link to another site is refused, so upload the file instead. A file of its own storage is not refused.
rights_confirmation_requiredThe deployment requires a confirmation, and none was sent.
rights_authorization_requiredThe deployment requires a confirmation, the import came through an API key, a connected app or an AI agent, and the account holds no standing authorization. The person authorizes in their own session, with contentRights.authorize().
rights_confirmation_not_foundNo confirmation has that id.
rights_confirmation_mismatchThe confirmation is for another link or another account.
rights_confirmation_staleThe declaration changed. Read it again and confirm again.
rights_confirmation_expiredThe confirmation is older than one hour.
rights_confirmation_usedAn import was already made under the confirmation. An import that failed after its fetch started counts too.

For a rights_confirmation_* code, call client.contentRights.confirm({ url, declarationVersion }) again and import again with the new id. ImportRefusedError extends ForbiddenError, so an existing instanceof ForbiddenError check still catches it. Check instanceof ImportRefusedError first, and read code to show your own sentence. IMPORT_REFUSAL_CODES, a ReadonlySet<ImportRefusalCode>, lists the codes. The constructor is new ImportRefusedError(code, message?).

NotFoundError

HTTP 404. The item does not exist, or it is not visible to this caller. Nodaro answers the same way in both cases, so an id never reveals whether something you cannot see exists. A resource that exists only on Nodaro Cloud, such as Studio productions or Recast, also answers 404 on a self-hosted install.

code is the server's own code, for example continuation_not_found. It is not_found when the server sent no code, as for a route that does not exist. The constructor is new NotFoundError(message?, code?).

Credits, storage and limits

InsufficientCreditsError

HTTP 402. The account cannot pay for the run, so nothing started. required is the number of credits the run needs, and available is the number the account has, or, for a cap, what the cap has left. Both are set by Nodaro Cloud, but the type marks them optional. code is the refusal's own code: insufficient_credits for your own balance, and when the server sent no code, or another 402 code such as free_video_limit or insufficient_app_credits.

try {
  await client.nodes.runAndWait("generate-video", params)
} catch (err) {
  if (err instanceof InsufficientCreditsError) {
    console.log(`Need ${err.required} credits, have ${err.available}`)
  }
}

Read the balance before a run with client.credits.balance(). See Credits.

CreditCapExceededError

HTTP 402 from a spending cap someone else set, not from your own balance. Topping up does not help: whoever set the cap has to raise it.

codeCap
budget_exceededThe workspace's budget
member_cap_exceededYour credit cap in that workspace
user_allowance_exceededThe deployment's per-user allowance

CreditCapExceededError extends InsufficientCreditsError, so an existing instanceof InsufficientCreditsError check still catches it. Check instanceof CreditCapExceededError first to tell the two apart. CREDIT_CAP_CODES, a ReadonlySet<CreditCapCode>, lists the three codes.

StorageExceededError

HTTP 413. What you sent does not fit. Every 413 arrives as this class, whatever the route, and reason says which kind it is:

reasonMeaning
storage_limit_exceededThe account's storage is full. limitBytes is the limit. Delete media you no longer need, then retry.
file_too_largeOne file is over the size limit. The storage is not full. Send a smaller file.
payload_too_large, post_too_large, duration_exceededThe request, the post or the media length is over a limit. The storage is not full.

reason is undefined when the answer carried no code. Uploads and copies into your storage, such as a community clone, throw this error. The constructor is new StorageExceededError(message?, limitBytes?, reason?).

RateLimitedError

HTTP 429. You sent too many requests. Wait, then retry with a growing pause: for example 2 seconds, then 4, then 8. Stop after a few attempts. See Rate limits.

code is the server's own code, for example rate_limit_exceeded, or rate_limited when the server sent none. The constructor is new RateLimitedError(message?, code?).

Waiting for jobs

client.nodes.runAndWait(), client.nodes.runMany() and the ...AndWait helpers of other resources poll a job until it ends. They throw these errors while they wait.

JobFailedError

The job ended with the status failed or cancelled. jobStatus says which, jobId names the job, and message carries the job's own error message. Read the full job with client.jobs.get(err.jobId): its error_hint explains a safety or policy block.

JobTimeoutError

The job did not reach an end state within maxMs, 15 minutes by default. The job is not cancelled. It usually still completes on the server and lands in your library. Fetch it later with client.jobs.get(err.jobId), or pass a larger maxMs for slow models. A job that the platform is recovering reports recovering: true in its status, and recovery can take tens of minutes.

JobAbortedError

Your AbortSignal fired. The SDK stops polling at once. The job is not cancelled. To stop it on the server and refund its reserved credits, call client.jobs.cancel(err.jobId).

JobHeldError

The job reached the status pending_review: a content policy of this deployment held the result for a person to review. The SDK stops waiting on the first poll that sees this status. The job is not cancelled, and its credits stay reserved during the review. Do not run the request again, because a duplicate would be held too.

Check the job later with client.jobs.get(err.jobId). It resolves to completed when the reviewer approves it, to failed when the reviewer rejects it, or to cancelled if you cancel it. A rejected job carries error_hint.kind set to policy-block and a reason you can show as it is. This error occurs only on deployments that register a job policy.

JobBlockedError

HTTP 422 with the code job_blocked. A job policy or content guard of this deployment refused the request before it ran. No job was created and nothing was charged. message is written for your users, so show it as it is. Do not retry the same request. This error occurs only on deployments that register a job policy.

Concurrent changes

WorkflowConflictError

HTTP 409. You made a conditional change, and someone else changed the item first. It arrives with one of two codes:

  • workflow_conflict: a client.workflows.update() with expectedVersion or expectedUpdatedAt did not match the stored workflow.
  • production_busy: a Studio production kept changing while the server applied your change, and the server stopped retrying.

The remedy is the same for both: read the item again, apply your change to the fresh copy, and send it again. When the server includes it, currentRecord holds the current workflow, so you can merge without another read.

import { WorkflowConflictError } from "@nodaro/sdk"

try {
  await client.workflows.update(id, { settings, expectedVersion: loadedVersion })
} catch (err) {
  if (err instanceof WorkflowConflictError && err.currentRecord) {
    const merged = mergeSettings(err.currentRecord.settings, settings)
    await client.workflows.update(id, { settings: merged, expectedVersion: err.currentVersion })
  } else {
    throw err
  }
}

AlreadyRunningError

HTTP 409 with the code already_running. The run you asked to start or continue is still running, and executionId names it. Follow that run with client.executions.get(executionId) instead of starting it again. client.workflows.run() throws it while a run of the workflow is under way. client.apps.resume() throws it while a Retry of the run is pending.

The class is chosen when the 409 carries an executionId. A 409 that names nothing stays a plain NodaroError.

AppRunVersionChangedError

HTTP 409 with the code app_run_version_changed. A Retry of a failed app run with client.apps.resume() was refused, because the app is on another version than the one the run ran. Nothing was written or billed.

  • runVersion: the version the run ran.
  • appVersion: the version you are on, that is the version you sent, or else the app's latest.

Both fields are undefined when the answer does not carry them. Start the run over with client.apps.run() to run the app's current version. To continue on the run's own version, call resume again with { version: err.runVersion }.

Locations and objects use their own conflict code, concurrent_modification, which arrives as a plain NodaroError. Handle it the same way: read the item again, merge, and retry.

Studio batches

These three classes belong to Studio productions.

  • StudioOpError: a batch of operations was refused, and opIndex is the zero-based position of the operation that caused it. Nothing in the batch was written. Fix that operation and send the whole batch again.
  • StudioPreviewUnavailable: you asked for a preview with dryRun: true, and this deployment cannot give one. Your batch was not sent. Tell the user a preview is not available, and do not apply the batch without asking.
  • StudioPreviewAppliedError: you asked for a preview, and the batch was applied anyway. Treat applied.production and applied.version as the current state. Do not send the batch again. When applied is undefined, read the production again before you decide anything.

Codes you may see on NodaroError

These codes arrive on a plain NodaroError. Compare err.code to handle them.

StatusCodeWhereMeaning
400validation_errorMany methodsA field is missing or invalid. message names it.
400no_valid_inputsclient.reduce.run()Every input was empty.
400invalid_edlclient.edit.applyEdl()The edit decision list failed validation.
400limit_reachedclient.developerApps.create()You already have the maximum number of apps.
400locked_fieldclient.apps.run()An override tried to change a destination, such as a webhook URL.
409name_takenCharacters, organizationsThe name or slug is already in use.
409concurrent_modificationLocations, objectsThe item changed since you read it.
410voice_cloning_retiredclient.voices.createClone()Voice cloning is no longer offered. Design a voice instead.
400quote_unsupportedclient.nodes.quote()The platform cannot quote this node type. The message lists the types it can quote.
400, 502advanced_mode_unavailableclient.llm.structured()The model's own provider cannot take Advanced-mode requests right now. Turn Advanced mode off to run the model.
409job_cancelledMetered LLM requests, such as client.llm.structured()The job was cancelled after its credits were held, but before its call was sent. Nothing is charged. Do not retry it as it is: the job was cancelled on purpose.
503job_start_failedMetered LLM requestsThe server could not record that the call is starting, so it did not send it. Nothing is charged. Retry it.
503provider_unavailableclient.llm.structuredJob()The instance cannot run this model. Do not retry.
503feature_disabledclient.copilotThe feature is switched off on this deployment.
503nodaro_connection_requiredclient.edit.editPlan()A self-hosted install needs a Nodaro Cloud connection for this.

Each reference page lists the codes of its own methods. The REST API errors page lists every code the API can send.

Retry safely

  • Retry reads freely. A get or list has no side effects.
  • Do not repeat a paid request blindly. A timed-out request may still have started a run. Before you retry a generation, pass an idempotency key: client.nodes.run(type, params, { idempotencyKey }) and runAndWait accept one. Reuse the same key when you retry the same request, and the platform returns the first run instead of starting and charging a second one.
  • Studio and Recast use their own retry tokens: clientRequestId on Studio methods and requestId on client.recast.rescore().
  • Retry 5xx answers with a pause. A custom fetch in createClient is a good place for that logic.

throwFromResponse(status, body)

throwFromResponse(status: number, body: {
  error?: { code?: string; message?: string; [key: string]: unknown }
}): never

Maps an HTTP status and a Nodaro error body to the matching error class and throws it. The SDK uses it for every response, and it is exported for custom transports that call the API without the client.

Prop

Type

import { throwFromResponse } from "@nodaro/sdk"

throwFromResponse(403, {
  error: { code: "insufficient_scope", message: "Missing scope", missingScope: "workflows:execute" },
})
// throws a ForbiddenError whose missingScope is "workflows:execute"

Most routes answer with the { error: { code, message } } envelope. A few older routes answer with other shapes, and the SDK reads those too, so their errors carry a real code and message:

Bodycodemessage
{ error: "<code>", message? }, for example from character trainingThe stringmessage, or the code when there is none
{ error: "<code>", error_description }, from OAuthThe stringerror_description
{ message, issues: [...] }, a validation answer with HTTP 400validation_errormessage
{ statusCode, error: "<reason phrase>", message }, for example a route the server does not haveinternal_errormessage

At HTTP 500 and above, the SDK reads only the code from these shapes, never their text. Such an error's message is its code, or "Request failed". The envelope's own message is read at every status.

The error class still follows the status: a 404 is a NotFoundError whatever the body says. A body with no readable text keeps the class's default message, such as "Not found". An InsufficientCreditsError reads required and available from the envelope, or the required and balance fields the server sends beside it. For a per-user allowance, available reads remaining.

Frequently asked questions

How do I catch Nodaro API errors in TypeScript?

Wrap the call in try/catch and test the error with instanceof, from the most specific class to NodaroError last. Every class is exported from @nodaro/sdk.

What happens when the account has too few credits?

The call throws InsufficientCreditsError with HTTP status 402 before any work starts. Its required and available fields tell you how many credits the run needs and how many the account has.

Does JobTimeoutError cancel the job?

No. The job keeps running and usually still completes. Fetch it later with client.jobs.get(jobId), or raise maxMs for slow models.

Should I retry after a RateLimitedError?

Yes, after a pause. Wait a few seconds, double the wait on each new 429, and stop after a few attempts.

Which code does a ForbiddenError carry?

The server's own code, for example insufficient_scope or node_not_available, so you can branch on it. The code is forbidden only when the server sent none. A refused model arrives as ModelUnavailableError, and a refused import of a link as ImportRefusedError.

Last updated on

On this page