# エラー

> Nodaro の TypeScript SDK がスローするすべてのエラーを、HTTP ステータス、コード、フィールドとともに説明し、クレジット、レート制限、競合、失敗したジョブへの対処法も扱います。

Source: https://nodaro.ai/ja/docs/developers/sdk/errors

Nodaro SDK が API の応答に対してスローする**エラー**は、すべて `NodaroError` かそのサブクラスのインスタンスです。エラーステータスで失敗したリクエストは、そのステータスに対応するサブクラスをスローします。run-and-wait のヘルパーは、ジョブが失敗、タイムアウト、停止したときに、それぞれ専用のサブクラスをスローします。具体的なクラスから先にキャッチし、`NodaroError` は最後にキャッチしてください。

## すべてのエラークラス
| クラス | ステータス | `code` | 追加のフィールド | スローされる条件 |
| --- | --- | --- | --- | --- |
| `NodaroError` | いずれか | サーバー自身のコードです | | 基底クラスで、より具体的なクラスがないすべてのエラーです |
| `UnauthorizedError` | 401 | `unauthorized` | | トークンが未指定か、期限切れか、無効です |
| `ForbiddenError` | 403 | `forbidden` | `missingScope?` | 権限がない、または OAuth トークンにスコープが不足しています |
| `NotFoundError` | 404 | `not_found` | | 項目が存在しない、またはあなたには見えません |
| `RateLimitedError` | 429 | `rate_limited` | | リクエストが多すぎます |
| `InsufficientCreditsError` | 402 | `insufficient_credits` | `required?`、`available?` | アカウントが、実行の料金を払えません |
| `StorageExceededError` | 413 | `storage_exceeded` | `limitBytes?` | アカウントのストレージがいっぱいです |
| `WorkflowConflictError` | 409 | `workflow_conflict` または `production_busy` | `currentUpdatedAt?`、`currentVersion?`、`currentRecord?` | 先に、ほかの人がその項目を変更しました |
| `JobBlockedError` | 422 | `job_blocked` | | デプロイ環境のコンテンツポリシーが、リクエストを拒否しました |
| `StudioOpError` | 4xx | サーバー自身のコードです | `opIndex` | スタジオのバッチ内の 1 つの操作が、拒否されました |
| `JobFailedError` | 0 | `job_failed` | `jobId`、`jobStatus` | 待っていたジョブが、失敗またはキャンセルされました |
| `JobTimeoutError` | 0 | `job_timeout` | `jobId`、`timeoutMs` | ジョブが `maxMs` 以内に終わりませんでした |
| `JobAbortedError` | 0 | `job_aborted` | `jobId?` | 待っている間に、自分の `AbortSignal` が発火しました |
| `JobHeldError` | 0 | `job_held` | `jobId` | ジョブが、人によるレビューのために保留されています |
| `StudioPreviewUnavailable` | 0 | `studio_preview_unavailable` | | デプロイ環境が、スタジオのバッチをプレビューできません |
| `StudioPreviewAppliedError` | 0 | `studio_preview_applied` | `applied` | プレビューを求めたスタジオのバッチが、適用されました |

どのクラスにも、3 つのフィールドがあります。読みやすい文である `message`、比較できる安定した文字列である `code`、HTTP ステータスである `status` です。`status` が `0` の場合、そのエラーは HTTP の応答から来たものではないことを意味します。たとえば `JobTimeoutError` は、SDK 自身のポーリングループがスローします。

一部のステータスは、サーバーが送ったコードにかかわらず、1 つのクラスに対応づけられます。403 は、`code` が `forbidden` に設定された `ForbiddenError` になり、404 は `NotFoundError` になります。これらの場合、サーバーの理由は `message` を読んでください。400、409、503 など、それ以外のステータスのエラーは、サーバー自身の `code` を持つ `NodaroError` として届きます。

## エラーを順番にキャッチする
```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
}
}
```

クライアントの `timeoutMs` を超えたリクエストと、ネットワークの障害は、どちらもランタイム自身のエラー（`AbortError` や `TypeError` など）を伴って失敗します。これらは `NodaroError` のインスタンスではありません。

## 認証と権限
### UnauthorizedError
HTTP 401 です。トークンが未指定か、期限切れか、無効です。新しいトークンを取得するか、ユーザーにもう一度ログインしてもらってから、再試行してください。

### ForbiddenError
HTTP 403 です。呼び出し元に、この操作を行う権限がありません。OAuth トークンに、エンドポイントが必要とするスコープが許可されていない場合、`missingScope` にそのスコープの名前が入ります。たとえば `workflows:execute` です。ユーザーにそのスコープを承認してもらい、新しいトークンで再試行してください。[スコープと権限の不足](https://nodaro.ai/docs/developers/sdk/auth#scopes-and-missing-permissions)を参照してください。

ほかの理由には、その機能を提供していないエディションであることや、ロールの権限が低すぎることがあります。いずれも `code` は `forbidden` になるため、どちらなのかは `message` を表示して説明してください。

### NotFoundError
HTTP 404 です。項目が存在しない、またはこの呼び出し元には見えないことを意味します。Nodaro はどちらの場合も同じように応答するため、ID によって、見えないものが存在するかどうかがわかることはありません。スタジオプロダクションや Recast のように、Nodaro Cloud にしか存在しないリソースも、セルフホスティング環境では 404 を返します。

## クレジット、ストレージ、上限
### InsufficientCreditsError
HTTP 402 です。アカウントが実行の料金を払えないため、何も開始されませんでした。`required` は実行に必要なクレジットの数、`available` はアカウントが持つクレジットの数です。どちらも Nodaro Cloud が設定しますが、型としては省略可能です。

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

実行の前に、[`client.credits.balance()`](https://nodaro.ai/docs/developers/sdk/models-and-credits) で残高を確認してください。[クレジット](https://nodaro.ai/docs/concepts/credits)を参照してください。

### StorageExceededError
HTTP 413 です。アカウントがストレージの上限に達したことを示し、`limitBytes` がその上限です。アップロードや、コミュニティの複製のようにストレージへのコピーを行う処理で、このエラーがスローされます。不要なメディアを削除してから、再試行してください。

### RateLimitedError
HTTP 429 です。リクエストを送りすぎました。待ってから、間隔を広げながら再試行してください。たとえば 2 秒、次に 4 秒、次に 8 秒です。何度か試したら止めてください。[レート制限](https://nodaro.ai/docs/developers/api/rate-limits)を参照してください。

## ジョブを待つ
`client.nodes.runAndWait()`、`client.nodes.runMany()`、そしてほかのリソースの `...AndWait` ヘルパーは、ジョブが終わるまでポーリングします。待っている間、これらは次のエラーをスローします。

### JobFailedError
ジョブが `failed` または `cancelled` のステータスで終わりました。どちらであるかは `jobStatus` が示し、`jobId` はそのジョブを、`message` はジョブ自身のエラーメッセージを示します。`client.jobs.get(err.jobId)` でジョブ全体を読み取ってください。その `error_hint` が、安全フィルターやポリシーによるブロックを説明します。

### JobTimeoutError
ジョブが `maxMs`（デフォルトは 15 分）以内に、終了状態に達しませんでした。**ジョブはキャンセルされません。**たいていはそのままサーバー上で完了し、あなたのライブラリに届きます。後から `client.jobs.get(err.jobId)` で取得するか、遅いモデルには、より大きな `maxMs` を渡してください。プラットフォームが復旧中のジョブは、そのステータスに `recovering: true` が示され、復旧には数十分かかることがあります。

### JobAbortedError
自分の `AbortSignal` が発火しました。SDK はただちにポーリングを止めます。**ジョブはキャンセルされません。**サーバー上でジョブを止め、確保されていたクレジットを返還するには、`client.jobs.cancel(err.jobId)` を呼び出してください。

### JobHeldError
ジョブが `pending_review` のステータスに達しました。このデプロイ環境のコンテンツポリシーが、人によるレビューのために結果を保留したことを意味します。SDK は、このステータスを最初に確認したポーリングで、待機を止めます。ジョブはキャンセルされず、レビューの間、クレジットは確保されたままです。リクエストをもう一度実行しないでください。重複したジョブも、同じように保留されるためです。

後から `client.jobs.get(err.jobId)` でジョブを確認してください。レビュー担当者が承認すると `completed` に、却下すると `failed` になり、あなたがキャンセルすると `cancelled` になります。却下されたジョブには、`policy-block` に設定された `error_hint.kind` と、そのまま表示できる `reason` が含まれます。このエラーが起きるのは、ジョブのポリシーを登録しているデプロイ環境だけです。

### JobBlockedError
HTTP 422 で、コードは `job_blocked` です。このデプロイ環境のコンテンツポリシーが、**実行される前に**リクエストを拒否しました。ジョブは作成されず、料金も発生していません。`message` はユーザー向けに書かれているため、そのまま表示してください。同じリクエストを再試行しないでください。このエラーが起きるのは、ジョブのポリシーを登録しているデプロイ環境だけです。

## 同時に行われた変更
### WorkflowConflictError
HTTP 409 です。条件付きの変更を行おうとしたところ、先にほかの人がその項目を変更していました。次の 2 つのコードのいずれかで届きます。

- `workflow_conflict`：`expectedVersion` または `expectedUpdatedAt` を指定した [`client.workflows.update()`](https://nodaro.ai/docs/developers/sdk/workflows) が、保存されているワークフローと一致しませんでした。
- `production_busy`：サーバーがあなたの変更を適用している間も、スタジオプロダクションが変化し続けたため、サーバーが再試行を止めました。

どちらも対処法は同じです。項目をもう一度読み取り、最新のコピーに変更を適用して、もう一度送信してください。サーバーが含めている場合、`currentRecord` に現在のワークフローが入っているため、もう一度読み取らずにマージできます。

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

ロケーションとオブジェクトは、独自の競合コードである `concurrent_modification` を使い、これは通常の `NodaroError` として届きます。対処法は同じです。項目をもう一度読み取り、マージしてから再試行してください。

## スタジオのバッチ
この 3 つのクラスは、[スタジオプロダクション](https://nodaro.ai/docs/developers/sdk/studio)に関するものです。

- **`StudioOpError`**：操作のバッチが拒否され、`opIndex` にはその原因となった操作の位置が、0 から数えて入ります。バッチ内のものは何も書き込まれていません。その操作を修正して、バッチ全体をもう一度送信してください。
- **`StudioPreviewUnavailable`**：`dryRun: true` を付けてプレビューを求めたものの、このデプロイ環境ではプレビューを提供できません。バッチは送信されていません。プレビューが利用できないことをユーザーに伝え、確認なしにバッチを適用しないでください。
- **`StudioPreviewAppliedError`**：プレビューを求めたにもかかわらず、バッチが適用されてしまいました。`applied.production` と `applied.version` を、現在の状態として扱ってください。バッチをもう一度送信しないでください。`applied` が `undefined` の場合は、何かを決める前に、プロダクションをもう一度読み取ってください。

## NodaroError で見られるコード
これらのコードは、通常の `NodaroError` に付いて届きます。`err.code` を比較して処理してください。

| ステータス | コード | 対象 | 意味 |
| --- | --- | --- | --- |
| 400 | `validation_error` | 多くのメソッド | フィールドが不足しているか、無効です。`message` にその名前が示されます。 |
| 400 | `no_valid_inputs` | `client.reduce.run()` | すべての入力が空でした。 |
| 400 | `invalid_edl` | `client.edit.applyEdl()` | 編集決定リストが、検証に失敗しました。 |
| 400 | `limit_reached` | `client.developerApps.create()` | すでにアプリの上限数に達しています。 |
| 400 | `locked_field` | `client.apps.run()` | オーバーライドが、Webhook の URL など、送信先を変更しようとしました。 |
| 409 | `name_taken` | キャラクター、組織 | 名前またはスラッグが、すでに使われています。 |
| 409 | `concurrent_modification` | ロケーション、オブジェクト | 読み取った後に、項目が変更されました。 |
| 410 | `voice_cloning_retired` | `client.voices.createClone()` | ボイスクローニングは、もう提供されていません。代わりにボイスデザインを使ってください。 |
| 503 | `provider_unavailable` | `client.llm.structuredJob()` | このインスタンスでは、このモデルを実行できません。再試行しないでください。 |
| 503 | `feature_disabled` | `client.copilot` | この機能は、このデプロイ環境ではオフになっています。 |
| 503 | `nodaro_connection_required` | `client.edit.editPlan()` | セルフホスティング環境でこれを使うには、Nodaro Cloud への接続が必要です。 |

各リファレンスページには、そのページ自身のメソッドのコードが一覧表示されています。[REST API のエラー](https://nodaro.ai/docs/developers/api/errors)ページには、API が送るすべてのコードが一覧表示されています。

## 安全に再試行する
- **読み取りは、自由に再試行できます。**`get` や `list` には、副作用がありません。
- **有料のリクエストを、むやみに繰り返さないでください。**タイムアウトしたリクエストでも、実行はすでに始まっている場合があります。生成を再試行する前に、冪等性キーを渡してください。`client.nodes.run(type, params, { idempotencyKey })` と `runAndWait` は、これを受け付けます。同じリクエストを再試行するときに同じキーを使うと、プラットフォームは 2 回目の実行を開始して課金する代わりに、最初の実行を返します。
- **スタジオと Recast** は、独自の再試行用トークンを使います。スタジオのメソッドでは `clientRequestId`、`client.recast.rescore()` では `requestId` です。
- **5xx の応答は、間を置いて再試行してください。**[`createClient`](https://nodaro.ai/docs/developers/sdk/client#timeouts-and-a-custom-fetch) 内でのカスタム `fetch` は、そのための処理を置くのに適した場所です。

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

HTTP ステータスと Nodaro のエラーボディを、対応するエラークラスに変換してスローします。SDK はすべての応答でこれを使っており、クライアントを使わずに API を呼び出すカスタムのトランスポートのためにも、これはエクスポートされています。

<TypeTable
type={{
status: { type: 'number', required: true, description: "応答の HTTP ステータスです。" },
body: { type: '{ error?: { code?, message?, ... } }', required: true, description: "パースされた JSON ボディです。missingScope、required、available、limitBytes、opIndex などの追加のフィールドが、対応するエラーのフィールドを埋めます。" },
}}
/>

```ts

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

## Frequently asked questions

### TypeScript で、Nodaro API のエラーをキャッチするには、どうすればよいですか？

呼び出しを try/catch で囲み、instanceof でエラーを判定します。最も具体的なクラスから調べ、最後に NodaroError を調べてください。すべてのクラスは、@nodaro/sdk からエクスポートされています。

### アカウントのクレジットが足りない場合、何が起きますか？

呼び出しは、処理が始まる前に、HTTP ステータス 402 の InsufficientCreditsError をスローします。その required と available フィールドで、実行に必要なクレジットの数と、アカウントが持つクレジットの数がわかります。

### JobTimeoutError は、ジョブをキャンセルしますか？

いいえ。ジョブは実行を続け、たいていはそのまま完了します。後から client.jobs.get(jobId) で取得するか、遅いモデルには maxMs を大きくしてください。

### RateLimitedError の後は、再試行すべきですか？

はい、少し待ってからです。数秒待ち、新しい 429 が出るたびに待ち時間を倍にし、何度か試したら止めてください。

### 403 エラーが、正確な理由のコードを示さないのはなぜですか？

403 は、すべて code が forbidden の ForbiddenError として届きます。理由は message を読んでください。OAuth のスコープが不足している場合は、missingScope も確認してください。
