Skip to content

Error Handling

All API errors follow the RFC 7807 Problem Details format. Every error response is a JSON object with a consistent structure, regardless of the HTTP status code:

{
"type": "https://api.studyplug.org/errors/not-found",
"title": "Not Found",
"status": 404,
"detail": "No skill found with slug 'add-within-99'.",
"instance": "/api/v1/skills/add-within-99"
}
FieldTypeDescription
typestringA URI identifying the error category. Stable across API versions.
titlestringA short, human-readable summary of the error type.
statusintegerThe HTTP status code.
detailstringA human-readable explanation specific to this occurrence.
instancestringThe request path that produced the error.
errorsarrayPresent on validation errors. Lists individual field-level issues.
retryAfterintegerPresent on rate-limit errors. Seconds until the client can retry.

Returned when the request body or query parameters fail schema validation.

{
"type": "https://api.studyplug.org/errors/bad-request",
"title": "Bad Request",
"status": 400,
"detail": "Request body failed validation.",
"instance": "/api/v1/generate",
"errors": [
{ "field": "count", "message": "Number must be greater than or equal to 1", "value": 0 },
{ "field": "grade", "message": "Required" }
]
}

The errors array provides field-level detail so you can highlight specific form inputs or log exact issues.

Returned when a referenced resource does not exist.

{
"type": "https://api.studyplug.org/errors/not-found",
"title": "Not Found",
"status": 404,
"detail": "No skill found with slug 'add-within-99'.",
"instance": "/api/v1/skills/add-within-99"
}

Returned when the request is syntactically valid but a parameter value is out of range or incompatible (e.g., count outside 1—50, or a seed that is not an integer).

{
"type": "https://api.studyplug.org/errors/validation",
"title": "Invalid Parameter",
"status": 422,
"detail": "'count' must be between 1 and 50",
"instance": "/api/v1/generate"
}

Returned when the client exceeds the request rate limit. The retryAfter field tells you how long to wait.

{
"type": "https://api.studyplug.org/errors/rate-limited",
"title": "Rate Limited",
"status": 429,
"detail": "Rate limit exceeded. You have exceeded 30 requests per minute.",
"instance": "/api/v1/generate",
"retryAfter": 23
}

Returned for unexpected server-side failures. These are logged and investigated by the StudyPlug team.

{
"type": "https://api.studyplug.org/errors/internal",
"title": "Generation Failed",
"status": 500,
"detail": "Content generation failed. Please try again.",
"instance": "/api/v1/generate"
}

The SDK throws typed errors that you can catch and inspect:

import { StudyPlug, isStudyPlugError, isRateLimitError, isValidationError } from "studyplug";
const client = new StudyPlug();
try {
const result = await client.generate({
skill: "add-within-10",
grade: "kindergarten",
count: 5,
});
console.log(result.data.items);
} catch (error) {
if (isRateLimitError(error)) {
const retryAfter = error.retryAfter ?? 60;
console.log(`Rate limited. Retrying in ${retryAfter} seconds...`);
} else if (isValidationError(error)) {
console.error(`Validation failed: ${error.raw.detail}`);
for (const fieldError of error.raw.errors ?? []) {
console.error(` Field '${fieldError.field}': ${fieldError.message}`);
}
} else if (isStudyPlugError(error)) {
console.error(`[${error.status}] ${error.raw.title}: ${error.raw.detail}`);
}
}

Check the status code first, then parse the JSON body:

const response = await fetch("https://api.studyplug.org/api/v1/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ skill: "add-within-10", grade: "kindergarten", count: 5 }),
});
if (!response.ok) {
const error = await response.json();
// error.type, error.title, error.status, error.detail are always present
throw new Error(`API error: ${error.detail}`);
}
const result = await response.json();

A recommended retry pattern for production applications:

import { isRateLimitError, isStudyPlugError } from "studyplug";
async function generateWithRetry(params: Parameters<typeof client.generate>[0], maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await client.generate(params);
} catch (error) {
if (isRateLimitError(error)) {
const wait = (error.retryAfter ?? 60) * 1000;
await new Promise((r) => setTimeout(r, wait));
continue;
}
if (isStudyPlugError(error) && error.status >= 500 && attempt < maxRetries) {
await new Promise((r) => setTimeout(r, 1000 * attempt));
continue;
}
throw error; // 4xx errors (except 429) are not retryable
}
}
}

All error responses are returned with Content-Type: application/problem+json as specified by RFC 7807. Most JSON parsers handle this transparently, but if you perform strict content-type checking, allow both application/json and application/problem+json.