# Erros

> Todos os erros do SDK do Nodaro para TypeScript, com status HTTP, código e campos, e o que fazer com créditos, limites de taxa, conflitos e jobs com falha.

Source: https://nodaro.ai/pt-BR/docs/developers/sdk/errors

Todo **erro** que o SDK do Nodaro lança para uma resposta da API é uma instância de `NodaroError` ou de uma das subclasses dele. Uma requisição que falha com um status de erro lança a subclasse correspondente ao status. Os helpers que executam e esperam lançam subclasses próprias quando um job falha, excede o tempo limite ou é interrompido. Capture primeiro as classes específicas e `NodaroError` por último.

## Todas as classes de erro
| Classe | Status | `code` | Campos extras | Quando é lançado |
| --- | --- | --- | --- | --- |
| `NodaroError` | Qualquer um | O código do servidor | | A classe base, e qualquer erro sem uma classe mais específica |
| `UnauthorizedError` | 401 | `unauthorized` | | O token está ausente, expirou ou é inválido |
| `ForbiddenError` | 403 | `forbidden` | `missingScope?` | A permissão foi negada, ou falta um escopo a um token OAuth |
| `NotFoundError` | 404 | `not_found` | | O item não existe, ou você não pode vê-lo |
| `RateLimitedError` | 429 | `rate_limited` | | Requisições demais |
| `InsufficientCreditsError` | 402 | `insufficient_credits` | `required?`, `available?` | A conta não consegue pagar a execução |
| `StorageExceededError` | 413 | `storage_exceeded` | `limitBytes?` | O armazenamento da conta está cheio |
| `WorkflowConflictError` | 409 | `workflow_conflict` ou `production_busy` | `currentUpdatedAt?`, `currentVersion?`, `currentRecord?` | Outra pessoa alterou o item antes |
| `JobBlockedError` | 422 | `job_blocked` | | A política de conteúdo da implantação recusou a requisição |
| `StudioOpError` | 4xx | O código do servidor | `opIndex` | Uma operação de um lote do Studio foi recusada |
| `JobFailedError` | 0 | `job_failed` | `jobId`, `jobStatus` | Um job que você estava esperando falhou ou foi cancelado |
| `JobTimeoutError` | 0 | `job_timeout` | `jobId`, `timeoutMs` | Um job não terminou dentro de `maxMs` |
| `JobAbortedError` | 0 | `job_aborted` | `jobId?` | Seu `AbortSignal` foi acionado durante a espera |
| `JobHeldError` | 0 | `job_held` | `jobId` | Um job está retido para revisão humana |
| `StudioPreviewUnavailable` | 0 | `studio_preview_unavailable` | | A implantação não consegue gerar a prévia de um lote do Studio |
| `StudioPreviewAppliedError` | 0 | `studio_preview_applied` | `applied` | Um lote do Studio do qual você pediu a prévia foi aplicado |

Toda classe tem três campos: `message`, uma frase legível; `code`, uma string estável que você pode comparar; e `status`, o status HTTP. Um `status` igual a `0` significa que o erro não veio de uma resposta HTTP. Por exemplo, `JobTimeoutError` é lançado pelo próprio loop de consulta periódica do SDK.

Alguns status correspondem a uma única classe, seja qual for o código que o servidor enviou. Um 403 vira `ForbiddenError` com `code` igual a `forbidden`, e um 404 vira `NotFoundError`. Nesses casos, leia `message` para saber o motivo do servidor. Um 409 só vira `WorkflowConflictError` com o código `workflow_conflict` ou `production_busy`. Erros com qualquer outro status, como 400 ou 503, e um 409 com qualquer outro código chegam como `NodaroError` com o `code` do próprio servidor.

## Capturar os erros em ordem
```ts

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
}
}
```

Uma requisição que excede o `timeoutMs` do cliente e uma falha de rede são rejeitadas com o erro do próprio runtime, como um `AbortError` ou um `TypeError`. Esses erros não são instâncias de `NodaroError`.

## Autenticação e permissão
### UnauthorizedError
HTTP 401. O token está ausente, expirou ou é inválido. Obtenha um novo token ou faça o usuário entrar de novo e, depois, tente outra vez.

### ForbiddenError
HTTP 403. Quem chamou não tem permissão para isso. Quando um token OAuth não recebeu um escopo de que o endpoint precisa, `missingScope` indica esse escopo, por exemplo `workflows:execute`. Peça ao usuário que o aprove e tente de novo com o novo token. Veja [Escopos e permissões ausentes](https://nodaro.ai/docs/developers/sdk/auth#scopes-and-missing-permissions).

Outros motivos incluem uma edição que não oferece o recurso e uma função abaixo da necessária. Todos chegam com `code` igual a `forbidden`, então mostre `message` para explicar qual é o caso.

### NotFoundError
HTTP 404. O item não existe ou não está visível para quem chamou. O Nodaro responde da mesma forma nos dois casos, então um ID nunca revela se existe algo que você não pode ver. Um recurso que só existe no Nodaro Cloud, como as produções do Studio ou o Recast, também responde 404 em uma instalação self-hosted.

## Créditos, armazenamento e limites
### InsufficientCreditsError
HTTP 402. A conta não consegue pagar a execução, então nada foi iniciado. `required` é o número de créditos que a execução exige, e `available` é o número que a conta tem. O Nodaro Cloud preenche os dois, mas o tipo os marca como opcionais.

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

Leia o saldo antes de uma execução com [`client.credits.balance()`](https://nodaro.ai/docs/developers/sdk/models-and-credits). Veja [Créditos](https://nodaro.ai/docs/concepts/credits).

### StorageExceededError
HTTP 413. A conta atingiu o limite de armazenamento, e `limitBytes` é esse limite. Uploads e cópias para o seu armazenamento, como um clone da comunidade, lançam esse erro. Exclua as mídias de que você não precisa mais e tente de novo.

### RateLimitedError
HTTP 429. Você enviou requisições demais. Espere e tente de novo com uma pausa crescente: por exemplo, 2 segundos, depois 4, depois 8. Pare depois de algumas tentativas. Veja [Limites de taxa](https://nodaro.ai/docs/developers/api/rate-limits).

## Espera por jobs
`client.nodes.runAndWait()`, `client.nodes.runMany()` e os helpers `...AndWait` de outros recursos consultam um job periodicamente até ele terminar. Eles lançam estes erros durante a espera.

### JobFailedError
O job terminou com o status `failed` ou `cancelled`. `jobStatus` diz qual dos dois, `jobId` identifica o job, e `message` traz a mensagem de erro do próprio job. Leia o job completo com `client.jobs.get(err.jobId)`: o `error_hint` dele explica um bloqueio de segurança ou de política.

### JobTimeoutError
O job não chegou a um estado final dentro de `maxMs`, que por padrão é de 15 minutos. **O job não é cancelado.** Em geral, ele termina mesmo assim no servidor e vai para a sua biblioteca. Busque o job mais tarde com `client.jobs.get(err.jobId)` ou passe um `maxMs` maior para modelos lentos. Um job que a plataforma está recuperando informa `recovering: true` no status, e a recuperação pode levar dezenas de minutos.

### JobAbortedError
Seu `AbortSignal` foi acionado. O SDK interrompe a consulta periódica na hora. **O job não é cancelado.** Para interrompê-lo no servidor e reembolsar os créditos reservados, chame `client.jobs.cancel(err.jobId)`.

### JobHeldError
O job chegou ao status `pending_review`: uma política de conteúdo desta implantação reteve o resultado para que uma pessoa o revise. O SDK para de esperar na primeira consulta que vê esse status. O job não é cancelado, e os créditos dele continuam reservados durante a revisão. Não execute a requisição de novo, porque uma duplicata também seria retida.

Verifique o job mais tarde com `client.jobs.get(err.jobId)`. Ele termina em `completed` quando o revisor o aprova, em `failed` quando o revisor o rejeita, ou em `cancelled` se você o cancelar. Um job rejeitado traz `error_hint.kind` igual a `policy-block` e um `reason` que você pode mostrar como está. Este erro só ocorre em implantações que registram uma política de jobs.

### JobBlockedError
HTTP 422 com o código `job_blocked`. Uma política de conteúdo desta implantação recusou a requisição **antes da execução**. Nenhum job foi criado e nada foi cobrado. `message` foi escrita para os seus usuários, então mostre-a como está. Não tente a mesma requisição de novo. Este erro só ocorre em implantações que registram uma política de jobs.

## Alterações simultâneas
### WorkflowConflictError
HTTP 409. Você fez uma alteração condicional, e outra pessoa alterou o item antes. Ele chega com um destes dois códigos:

- `workflow_conflict`: um [`client.workflows.update()`](https://nodaro.ai/docs/developers/sdk/workflows) com `expectedVersion` ou `expectedUpdatedAt` não correspondeu ao workflow armazenado.
- `production_busy`: uma produção do Studio continuou mudando enquanto o servidor aplicava a sua alteração, e o servidor parou de tentar.

A solução é a mesma para os dois: leia o item de novo, aplique sua alteração à cópia atualizada e envie de novo. Quando o servidor o inclui, `currentRecord` contém o workflow atual, então você pode mesclar sem outra leitura.

```ts

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
}
}
```

Locais e objetos usam um código de conflito próprio, `concurrent_modification`, que chega como um `NodaroError` simples. Trate-o da mesma forma: leia o item de novo, mescle e tente outra vez.

## Lotes do Studio
Estas três classes pertencem às [produções do Studio](https://nodaro.ai/docs/developers/sdk/studio).

- **`StudioOpError`**: um lote de operações foi recusado, e `opIndex` é a posição, contada a partir de zero, da operação que causou a recusa. Nada do lote foi gravado. Corrija essa operação e envie o lote inteiro de novo.
- **`StudioPreviewUnavailable`**: você pediu uma prévia com `dryRun: true`, e esta implantação não consegue gerá-la. Seu lote não foi enviado. Avise o usuário de que a prévia não está disponível e não aplique o lote sem perguntar.
- **`StudioPreviewAppliedError`**: você pediu uma prévia, e o lote foi aplicado mesmo assim. Trate `applied.production` e `applied.version` como o estado atual. Não envie o lote de novo. Quando `applied` for `undefined`, leia a produção de novo antes de decidir qualquer coisa.

## Códigos que podem aparecer em NodaroError
Estes códigos chegam em um `NodaroError` simples. Compare `err.code` para tratá-los.

| Status | Código | Onde | Significado |
| --- | --- | --- | --- |
| 400 | `validation_error` | Muitos métodos | Um campo está ausente ou é inválido. `message` indica qual. |
| 400 | `no_valid_inputs` | `client.reduce.run()` | Todas as entradas estavam vazias. |
| 400 | `invalid_edl` | `client.edit.applyEdl()` | A lista de decisões de edição não passou na validação. |
| 400 | `limit_reached` | `client.developerApps.create()` | Você já tem o número máximo de apps. |
| 400 | `locked_field` | `client.apps.run()` | Uma substituição tentou alterar um destino, como a URL de um webhook. |
| 409 | `name_taken` | Personagens, organizações | O nome ou o slug já está em uso. |
| 409 | `concurrent_modification` | Locais, objetos | O item mudou desde que você o leu. |
| 410 | `voice_cloning_retired` | `client.voices.createClone()` | A clonagem de voz não é mais oferecida. Use o design de voz para criar uma voz. |
| 503 | `provider_unavailable` | `client.llm.structuredJob()` | A instância não consegue executar este modelo. Não tente de novo. |
| 503 | `feature_disabled` | `client.copilot` | O recurso está desativado nesta implantação. |
| 503 | `nodaro_connection_required` | `client.edit.editPlan()` | Uma instalação self-hosted precisa de uma conexão com o Nodaro Cloud para isso. |

Cada página de referência lista os códigos dos próprios métodos. A página [Erros da API REST](https://nodaro.ai/docs/developers/api/errors) lista todos os códigos que a API pode enviar.

## Tentar de novo com segurança
- **Repita leituras à vontade.** Um `get` ou um `list` não tem efeitos colaterais.
- **Não repita às cegas uma requisição paga.** Uma requisição que excedeu o tempo limite pode ter iniciado uma execução mesmo assim. Antes de repetir uma geração, passe uma chave de idempotência: `client.nodes.run(type, params, { idempotencyKey })` e `runAndWait` aceitam uma. Reutilize a mesma chave ao repetir a mesma requisição, e a plataforma retorna a primeira execução em vez de iniciar e cobrar uma segunda.
- **Studio e Recast** usam tokens de nova tentativa próprios: `clientRequestId` nos métodos do Studio e `requestId` em `client.recast.rescore()`.
- **Repita respostas 5xx com uma pausa.** Um `fetch` personalizado em [`createClient`](https://nodaro.ai/docs/developers/sdk/client#timeouts-and-a-custom-fetch) é um bom lugar para essa lógica.

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

Converte um status HTTP e um corpo de erro do Nodaro na classe de erro correspondente e a lança. O SDK usa essa função em todas as respostas, e ela é exportada para transportes personalizados que chamam a API sem o cliente.

<TypeTable
type={{
status: { type: 'number', required: true, description: "O status HTTP da resposta." },
body: { type: '{ error?: { code?, message?, ... } }', required: true, description: "O corpo JSON, já convertido em objeto. Campos extras, como missingScope, required, available, limitBytes e opIndex, preenchem os campos correspondentes do erro." },
}}
/>

```ts

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

## Frequently asked questions

### Como capturar erros da API do Nodaro em TypeScript?

Envolva a chamada em try/catch e teste o erro com instanceof, da classe mais específica até NodaroError, por último. Todas as classes são exportadas por @nodaro/sdk.

### O que acontece quando a conta não tem créditos suficientes?

A chamada lança InsufficientCreditsError com status HTTP 402 antes de qualquer trabalho começar. Os campos required e available informam quantos créditos a execução exige e quantos a conta tem.

### JobTimeoutError cancela o job?

Não. O job continua em execução e, em geral, termina mesmo assim. Busque o job mais tarde com client.jobs.get(jobId) ou aumente maxMs para modelos lentos.

### Devo tentar de novo depois de um RateLimitedError?

Sim, depois de uma pausa. Espere alguns segundos, dobre a espera a cada novo 429 e pare depois de algumas tentativas.

### Por que um erro 403 não informa o código exato do motivo?

Todo 403 chega como ForbiddenError com o código forbidden. Leia o message do erro para saber o motivo, e o missingScope quando faltar um escopo OAuth.
