Skip to content

Error Handling

The SDK maps every non-2xx API response to a typed error class. All errors extend StudyPlugError, and the SDK exports type guard functions so you can handle each case cleanly without instanceof checks.

StudyPlugError (base — any non-2xx response)
├── AuthenticationError (401 / 403)
├── RateLimitError (429)
├── NotFoundError (404)
└── ValidationError (422)

Every SDK error has these properties:

class StudyPlugError extends Error {
readonly status: number; // HTTP status code (0 for network/timeout)
readonly raw: ApiErrorBody; // Full RFC 7807 Problem Details body
}

The raw object contains the complete error response:

interface ApiErrorBody {
type: string; // error type URI
title: string; // short description
status: number; // HTTP status code
detail: string; // human-readable explanation
instance: string; // request URL
errors?: ValidationErrorDetail[];
retryAfter?: number;
}

Thrown on 401 or 403 responses. Indicates a missing or invalid API key.

import { isAuthenticationError } from "studyplug";
try {
await sp.skills.list();
} catch (err) {
if (isAuthenticationError(err)) {
console.log(err.status); // 401
console.log(err.message); // "Invalid API key"
}
}

Thrown on 429 responses after all automatic retries are exhausted. The SDK retries 429s automatically (default: 2 retries with Retry-After backoff) before throwing.

import { isRateLimitError } from "studyplug";
try {
await sp.generate({ skill: "add-within-10", count: 50 });
} catch (err) {
if (isRateLimitError(err)) {
console.log(err.retryAfter); // seconds until you can retry
console.log(err.status); // 429
}
}
PropertyTypeDescription
retryAfternumberSeconds to wait before retrying (from Retry-After header or response body, defaults to 60)

Thrown on 404 responses. A requested resource does not exist.

import { isNotFoundError } from "studyplug";
try {
await sp.skills.get("nonexistent-skill");
} catch (err) {
if (isNotFoundError(err)) {
console.log(err.message); // "Skill 'nonexistent-skill' not found"
}
}

Thrown on 422 responses. The request body or query parameters failed validation.

import { isValidationError } from "studyplug";
try {
await sp.generate({ count: 999 });
} catch (err) {
if (isValidationError(err)) {
for (const fieldError of err.fieldErrors) {
console.log(`${fieldError.field}: ${fieldError.message}`);
// "count: Number must be less than or equal to 50"
}
}
}
PropertyTypeDescription
fieldErrorsValidationErrorDetail[]Array of { field, message, value? }

Network failures and timeouts also throw StudyPlugError with status: 0:

import { isStudyPlugError } from "studyplug";
try {
await sp.health();
} catch (err) {
if (isStudyPlugError(err) && err.status === 0) {
console.log(err.message);
// "Request timed out after 30000ms"
// or "Failed to fetch" (network error)
}
}

Use type guards instead of instanceof for cleaner code and better tree-shaking:

import {
isStudyPlugError,
isAuthenticationError,
isRateLimitError,
isNotFoundError,
isValidationError,
} from "studyplug";
GuardMatches
isStudyPlugError(err)Any SDK error
isAuthenticationError(err)401 / 403
isRateLimitError(err)429
isNotFoundError(err)404
isValidationError(err)422
import {
isRateLimitError,
isNotFoundError,
isValidationError,
isStudyPlugError,
} from "studyplug";
try {
const { data } = await sp.generate({ skill: "add-within-10", count: 5 });
// use data...
} catch (err) {
if (isRateLimitError(err)) {
// Back off and retry
await sleep(err.retryAfter * 1000);
} else if (isNotFoundError(err)) {
// Skill does not exist
console.error("Unknown skill");
} else if (isValidationError(err)) {
// Bad request parameters
console.error(err.fieldErrors);
} else if (isStudyPlugError(err)) {
// Other API error (500, etc.)
console.error(`API error ${err.status}: ${err.message}`);
} else {
throw err; // Not from StudyPlug SDK
}
}