Errors
Every answer that is not 2xx throws a PrismDraftError. It carries the API's error code, which is what your code should branch on.
PrismDraftError
The API answers failures with one shape, { "error": { "code", "message", "details" } }. The SDK turns it into an Error with these fields:
| Field | Type | Meaning |
|---|---|---|
status |
number |
The HTTP status. |
code |
string |
One of the codes below. |
message |
string |
A readable description, in English. Show it; do not parse it. |
details |
Record<string, unknown> | undefined |
Extra data for some codes, listed below. Absent when there is none. |
code is typed string, not a union, so a code added to the API later does not break a switch.
An answer that is not this shape, such as a bare 502 from a gateway, is also a PrismDraftError: its status is the HTTP status, code is INTERNAL_ERROR, and message is the status text.
A 2xx answer whose body is not JSON, such as an HTML page from a proxy in front of the API, is a PrismDraftError as well, for any method: status is the HTTP status (200, say), code is INVALID_RESPONSE, and message is The API returned a non-JSON body. The message holds neither the body nor the URL. The SDK does not retry it. An empty 2xx body is not an error.
Codes
| Code | Status | Means | details |
|---|---|---|---|
UNAUTHORIZED |
401 | The key is missing, unknown, expired or revoked. | |
FORBIDDEN |
403 | The key's role lacks a permission, or the key is not a member of a restricted project. | { permission }, or { projectId } |
RESOURCE_NOT_FOUND |
404 | No such resource, no such route, or a resource in another workspace. | { id } for a resource |
INVALID_REQUEST |
400 | The request is malformed or a value is refused. Also 413 (a JSON body over 1 MiB) and 415 (a content type the route does not take). | { issues } for validation |
CONFLICT |
409 | The state of the resource does not allow it, for example publishing an article that is not approved. | varies |
INSUFFICIENT_CREDITS |
402 | The workspace's credit balance does not cover the operation. | { required, available } |
QUOTA_EXCEEDED |
429 | A plan quota is used up, or the request rate limit was hit. | { metric, limit, used } for a quota |
RATE_LIMITED |
429 | An action was repeated too soon (resending an invitation within a minute), or a daily cap was reached (10 image style runs per workspace in 24 hours). The Retry-After header says how long to wait. |
{ retryAfterSeconds }, plus { limit, used } for a cap |
PAYMENT_FAILED |
402 | A billing operation failed, or billing is not configured on the deployment. | |
GENERATION_FAILED |
502 | A generation could not finish, or an AI call could not start. A failed job records it as job.errorCode (pd.generation.getStatus). |
{ reason, capability } when the AI is paused or not set up |
INTERNAL_ERROR |
500 | A server fault, or an answer that was not the API's error shape. | |
PROVIDER_RATE_LIMITED |
429 | In the catalogue; no route sends it today. | |
INVALID_STRIPE_EVENT |
400 | A Stripe event the API refused. Stripe calls that route, not the SDK. |
Two details:
- 404 is not always "does not exist". A workspace or resource you have no access to answers
RESOURCE_NOT_FOUND, the same as one that is not there.FORBIDDENmeans you can see it exists but your role may not do this. - Validation runs before authentication. A request that is malformed answers
400even when the key is wrong; fix the request first, then the key.
details
For INVALID_REQUEST, details.issues lists what failed. Each issue has a path (a dotted name such as title or translations.0.name) and a message. A Markdown import that cannot be converted lists { construct, line } instead, and an image that cannot be downloaded sets details.src.
For GENERATION_FAILED, the code has two sources. A run that started and failed is recorded on the job (job.errorCode). The same code is also a synchronous 502 answer when the deployment's AI cannot serve the call at all, before any credit is reserved or job created. It comes from generation start, outline approval, translations, category suggestions, planning ideas, publishing plan suggestions (plans.suggest), briefs, links and social posts, AI edit, and image regeneration. These answers carry details.reason, either ai_paused (an administrator paused the capability) or ai_not_configured (no provider or key is set up), and details.capability, one of text, image, embedding or search. Nothing was charged, so there is nothing to refund. The SDK does not retry a 502 from these calls, because it retries only GET requests; fix the deployment's settings, or ask its administrator, and send the request again.
For FORBIDDEN, details.permission names the missing permission, which is the quickest way to find the role you need.
For CONFLICT, details.reason names the cause when the API has one. article_state_changed means the article changed status while a request was moving it (for example it was approved, unpublished or edited by someone else between the API reading it and writing the new status), so nothing was changed; details.from and details.to give the status it was read in and the one asked for. Read the article again and, if the move still applies, send the request again. The SDK does not retry it for you, because it retries only GET requests.
Handle errors by code
import { PrismDraftError } from "@prism-draft/sdk";
type Outcome = "published" | "not approved yet" | "not allowed" | "no such article";
async function tryPublish(): Promise<Outcome> {
try {
await pd.articles.publish({ articleId });
return "published";
} catch (error) {
if (!(error instanceof PrismDraftError)) throw error;
switch (error.code) {
case "CONFLICT":
return "not approved yet"; // only an APPROVED article can be published
case "FORBIDDEN":
return "not allowed"; // error.details?.permission says which permission is missing
case "RESOURCE_NOT_FOUND":
return "no such article";
default:
throw error;
}
}
}
Tell an unavailable AI apart from a failed run:
import { PrismDraftError } from "@prism-draft/sdk";
try {
await pd.generation.start({ articleId });
} catch (error) {
if (error instanceof PrismDraftError && error.code === "GENERATION_FAILED") {
const reason = error.details?.reason; // "ai_paused" | "ai_not_configured"
const capability = error.details?.capability; // "text" | "image" | "embedding" | "search"
console.log(`AI unavailable: ${String(reason)} (${String(capability)})`);
} else {
throw error;
}
}
Read the validation issues of a refused request:
import { PrismDraftError } from "@prism-draft/sdk";
interface Issue {
path?: string;
message?: string;
construct?: string;
line?: number;
}
function issuesOf(error: PrismDraftError): Issue[] {
const issues = error.details?.issues;
return Array.isArray(issues) ? (issues as Issue[]) : [];
}
try {
await pd.articles.create({ body: { projectId, title: "" } });
} catch (error) {
if (error instanceof PrismDraftError && error.code === "INVALID_REQUEST") {
for (const issue of issuesOf(error)) console.log(`${issue.path}: ${issue.message}`);
} else {
throw error;
}
}
Other things a call can throw
A PrismDraftError means the API answered. Other failures are different errors:
| Thrown | When |
|---|---|
PrismDraftTimeoutError |
An attempt took longer than timeoutMs. It is an Error with timeoutMs, not a PrismDraftError: the API never answered. See Retries and timeouts. |
| The signal's reason | You aborted the call with your signal. For controller.abort() with no argument that is an AbortError. |
The error fetch rejected with |
The request did not reach the API (DNS, connection reset). A GET is retried first; see Retries and timeouts. |
TypeError |
A constructor option is invalid, or a path parameter is empty, . or ... These are mistakes in your code, raised before any request. |
WebhookSignatureError, WebhookPayloadError, UnknownWebhookEventError |
A webhook delivery you verify. See Webhooks. |
import { PrismDraftError, PrismDraftTimeoutError } from "@prism-draft/sdk";
try {
await pd.articles.get({ articleId }, { timeoutMs: 5_000 });
} catch (error) {
if (error instanceof PrismDraftTimeoutError) {
console.log(`no answer within ${error.timeoutMs} ms`);
} else if (error instanceof PrismDraftError) {
console.log(error.status, error.code, error.message);
} else {
throw error;
}
}