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
| Class | Status | code | Extra fields | Thrown when |
|---|---|---|---|---|
NodaroError | Any | The server's code | The base class, and any error without a more specific class | |
UnauthorizedError | 401 | unauthorized | The token is missing, expired or invalid | |
ForbiddenError | 403 | forbidden | missingScope? | Permission is denied, or an OAuth token lacks a scope |
NotFoundError | 404 | not_found | The item does not exist, or you cannot see it | |
RateLimitedError | 429 | rate_limited | Too many requests | |
InsufficientCreditsError | 402 | insufficient_credits | required?, available? | The account cannot pay for the run |
StorageExceededError | 413 | storage_exceeded | limitBytes? | The account's storage is full |
WorkflowConflictError | 409 | workflow_conflict or production_busy | currentUpdatedAt?, currentVersion?, currentRecord? | Someone else changed the item first |
JobBlockedError | 422 | job_blocked | The deployment's content policy refused the request | |
StudioOpError | 4xx | The server's code | opIndex | One operation in a Studio batch was refused |
JobFailedError | 0 | job_failed | jobId, jobStatus | A job you were waiting for failed or was cancelled |
JobTimeoutError | 0 | job_timeout | jobId, timeoutMs | A job did not finish within maxMs |
JobAbortedError | 0 | job_aborted | jobId? | Your AbortSignal fired while waiting |
JobHeldError | 0 | job_held | jobId | A job is held for human review |
StudioPreviewUnavailable | 0 | studio_preview_unavailable | The deployment cannot preview a Studio batch | |
StudioPreviewAppliedError | 0 | studio_preview_applied | applied | A 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: aclient.workflows.update()withexpectedVersionorexpectedUpdatedAtdid 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, andopIndexis 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 withdryRun: 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. Treatapplied.productionandapplied.versionas the current state. Do not send the batch again. Whenappliedisundefined, 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.
| Status | Code | Where | Meaning |
|---|---|---|---|
| 400 | validation_error | Many methods | A field is missing or invalid. message names it. |
| 400 | no_valid_inputs | client.reduce.run() | Every input was empty. |
| 400 | invalid_edl | client.edit.applyEdl() | The edit decision list failed validation. |
| 400 | limit_reached | client.developerApps.create() | You already have the maximum number of apps. |
| 400 | locked_field | client.apps.run() | An override tried to change a destination, such as a webhook URL. |
| 409 | name_taken | Characters, organizations | The name or slug is already in use. |
| 409 | concurrent_modification | Locations, objects | The item changed since you read it. |
| 410 | voice_cloning_retired | client.voices.createClone() | Voice cloning is no longer offered. Design a voice instead. |
| 503 | provider_unavailable | client.llm.structuredJob() | The instance cannot run this model. Do not retry. |
| 503 | feature_disabled | client.copilot | The feature is switched off on this deployment. |
| 503 | nodaro_connection_required | client.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
getorlisthas 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 })andrunAndWaitaccept 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:
clientRequestIdon Studio methods andrequestIdonclient.recast.rescore(). - Retry 5xx answers with a pause. A custom
fetchincreateClientis a good place for that logic.
throwFromResponse(status, body)
throwFromResponse(status: number, body: {
error?: { code?: string; message?: string; [key: string]: unknown }
}): neverMaps 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
Related
TypeScript SDK
Jobs and executions
Run nodes
Errors
Rate limits
Last updated on
Authentication
Choose how the Nodaro SDK authenticates, with StaticTokenAuth, CallbackAuth or supabaseAuth, and share one browser login across your subdomains.
Workflows and projects
Create, update, share, export, import and run Nodaro workflows from TypeScript with client.workflows, and organize them in projects with client.projects.