Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
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
ForbiddenError403forbiddenmissingScope?Permission is denied, or an OAuth token lacks a scope
NotFoundError404not_foundThe item does not exist, or you cannot see it
RateLimitedError429rate_limitedToo many requests
InsufficientCreditsError402insufficient_creditsrequired?, available?The account cannot pay for the run
StorageExceededError413storage_exceededlimitBytes?The account's storage is full
WorkflowConflictError409workflow_conflict or production_busycurrentUpdatedAt?, currentVersion?, currentRecord?Someone else changed the item first
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 three fields: message, a readable sentence; code, a stable string you can compare; and status, the HTTP status. 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.

Some statuses map to one class, whatever code the server sent. A 403 becomes ForbiddenError with code set to forbidden, and a 404 becomes NotFoundError. Read message for the server's reason in those cases. Errors with any other status, such as 400, 409 or 503, arrive as NodaroError with the server's own code.

Catch errors in order

import {
  ForbiddenError,
  InsufficientCreditsError,
  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 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) {
    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. They all arrive with code set to forbidden, so show message to explain which.

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.

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. Both are set by Nodaro Cloud, but the type marks them optional.

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.

StorageExceededError

HTTP 413. The account has reached its storage limit, and limitBytes is that limit. Uploads and copies into your storage, such as a community clone, throw it. Delete media you no longer need, then retry.

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.

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 content policy 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
  }
}

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.
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"

Frequently asked questions

Last updated on

On this page