Error Handling
Every KalimaLab error has a machine-readable code, a human-readable message, and an HTTP status code.
Error response format
On error, the data field is null and the error, the data field is null and the
error-response.json
{ "data": null, "error": { "code": "ERR_VALIDATION", "message": "Parameter 'letters' must be an integer between 1 and 20.", "field": "letters" }, "meta": { "requestId": "req_01j9abc...", "responseTimeMs": 2 }}| Field | Type | Description |
|---|---|---|
| error.code | string | Machine-readable error identifier. Use this for programmatic error handling. |
| error.message | string | Human-readable description. Safe to display to developers, not to end users. |
| error.field | string? | Present on ERR_VALIDATION — the name of the invalid parameter. |
| error.limit | number? | Present on ERR_RATE_LIMIT_EXCEEDED — your daily limit. |
| error.resetAt | string? | Present on ERR_RATE_LIMIT_EXCEEDED — ISO 8601 timestamp of when the limit resets. |
All error codes
| Code | HTTP | Description | Action |
|---|---|---|---|
| ERR_AUTH_MISSING | 401 | No API key provided. | Add an Authorization: Bearer ... or x-api-key header. |
| ERR_AUTH_INVALID | 401 | API key not found. | The key does not match any active key. Check for typos. |
| ERR_AUTH_REVOKED | 401 | API key revoked. | Create a new key from the dashboard. |
| ERR_RATE_LIMIT_EXCEEDED | 429 | Daily rate limit exceeded. | Check the X-RateLimit-Reset header for when the limit resets. |
| ERR_VALIDATION | 400 | Invalid request parameters. | The error message identifies the invalid field. |
| ERR_NOT_FOUND | 404 | Resource not found. | The requested word, root, or resource ID does not exist. |
| ERR_PLAN_LIMIT | 403 | Feature not available on current plan. | Upgrade to access this endpoint or parameter. |
| ERR_INTERNAL | 500 | Internal server error. | An unexpected error occurred. Retry with exponential backoff. |
Error handling in the SDK
The SDK exports typed error classes for each error code. Use instanceof checks for precise handling:
error-handling.ts
import { KalimaLab, RateLimitError, AuthError, NotFoundError, ValidationError, KalimaLabError,} from '@kalimalab/sdk'const client = new KalimaLab({ apiKey: process.env.KALIMALAB_API_KEY! })async function getWord(id: string) { try { return await client.words.get(id) } catch (err) { if (err instanceof RateLimitError) { // Specific: rate limit info available console.error(`Rate limit hit. Limit: ${err.limit}, resets at: ${err.resetAt}`) return null } if (err instanceof AuthError) { // Auth problem — check key console.error(`Auth error [${err.code}]: ${err.message}`) throw err } if (err instanceof NotFoundError) { // Word doesn't exist return null } if (err instanceof ValidationError) { // Bad parameters console.error(`Validation error on field: ${err.field}`) throw err } if (err instanceof KalimaLabError) { // Any other KalimaLab API error console.error(`API error ${err.status}: ${err.code}`) } throw err }}Error handling with native fetch
typescript
async function fetchWord(id: string) { const res = await fetch(`https://api.kalimalab.com/v1/words/${id}`, { headers: { Authorization: `Bearer ${process.env.KALIMALAB_API_KEY}` }, }) const body = await res.json() if (!res.ok) { const { code, message } = body.error switch (code) { case 'ERR_NOT_FOUND': return null case 'ERR_RATE_LIMIT_EXCEEDED': throw new Error(`Rate limited until ${body.error.resetAt}`) case 'ERR_AUTH_MISSING': case 'ERR_AUTH_INVALID': case 'ERR_AUTH_REVOKED': throw new Error(`Authentication failed: ${message}`) default: throw new Error(`API error ${res.status}: ${code}`) } } return body.data}Retry strategy
Not every error is worth retrying. Here is the recommended retry matrix:
| Error / Status | Retry? | Strategy |
|---|---|---|
| 400 ERR_VALIDATION | No | Fix request parameters. |
| 401 ERR_AUTH_* | No | Fix your API key. |
| 403 ERR_PLAN_LIMIT | No | Upgrade your plan. |
| 404 ERR_NOT_FOUND | No | Resource does not exist. |
| 429 ERR_RATE_LIMIT | Yes | Wait until the X-RateLimit-Reset timestamp. |
| 500 ERR_INTERNAL | Yes | Exponential backoff: 1s, 2s, 4s, then fail. |
| Network timeout | Yes | Retry up to 3 times with backoff. |
ℹSDK automatic retry
The SDK automatically retries 500 errors and network timeouts up to
maxRetries times (default: 3) with exponential backoff. It does not retry 4xx errors.Implementing exponential backoff
When calling the API directly (without the SDK), implement exponential backoff for retryable errors. This avoids flooding the API and increases the chance of success:
retry.ts
async function fetchWithRetry( url: string, options: RequestInit, maxRetries = 3,): Promise<Response> { let attempt = 0 while (attempt <= maxRetries) { const res = await fetch(url, options) // Success or a non-retryable client error — return immediately if (res.ok || (res.status >= 400 && res.status < 500 && res.status !== 429)) { return res } // 429: honour the Retry-After / X-RateLimit-Reset header if present if (res.status === 429) { const resetHeader = res.headers.get('X-RateLimit-Reset') const waitMs = resetHeader ? Math.max(0, Number(resetHeader) * 1000 - Date.now()) : 2 ** attempt * 1000 console.warn(`Rate limited. Waiting ${waitMs}ms before retry ${attempt + 1}.`) await new Promise((r) => setTimeout(r, waitMs)) } else { // 5xx: exponential backoff — 1s, 2s, 4s, 8s … const waitMs = 2 ** attempt * 1000 console.warn(`Server error ${res.status}. Waiting ${waitMs}ms before retry ${attempt + 1}.`) await new Promise((r) => setTimeout(r, waitMs)) } attempt++ } throw new Error(`Request failed after ${maxRetries} retries`)}// Usageconst res = await fetchWithRetry( 'https://api.kalimalab.com/v1/words?letters=3', { headers: { Authorization: `Bearer ${process.env.KALIMALAB_API_KEY}` } },)const { data } = await res.json()