Error Handling
The Reasoning Layer SDK uses a typed error hierarchy that lets you catch specific error conditions with instanceof checks. All errors extend a common base class, and the SDK handles rate limiting and retries automatically.
Error hierarchy
Section titled “Error hierarchy”ReasoningLayerError (base, extends Error) ├── ApiError (HTTP 4xx/5xx: .status, .errorCode, .body, .headers) │ ├── BadRequestError (400) │ ├── AuthenticationError (401) │ ├── ForbiddenError (403) │ ├── NotFoundError (404) │ ├── ConstraintViolationError (409: .termId, .feature, .constraint) │ ├── VocabularyRefusedError (governed vocabulary: .code, .current, .findings, .conflicts, .authority, .plugin, .refusal) │ │ ├── IdempotencyKeyRequiredError (428) │ │ ├── IdempotencyKeyReusedError (422: .storedRequestHash) │ │ └── MutationOutcomeUnconfirmedError (503: .recovery) │ ├── RateLimitError (429: .retryAfter, .limit, .remaining) │ └── InternalServerError (500+) │ └── ServiceUnavailableError (503: a component this deployment has not configured) ├── TimeoutError (.timeoutMs) ├── ValidationError (.field) ├── NetworkError └── ResponseShapeError (2xx body the SDK cannot read: .status, .method, .path, .field, .body)ResponseShapeError is not an ApiError: the request succeeded, but the body departs from the
published shape. No retry and no status logic applies to it.
A sendMessageStream error event is not thrown. To map it to the class that the blocking call
throws, call createApiError(event.status, { error: event.error, message: event.error }, new Headers()).
VocabularyRefusedError is chosen by the closed code of the body, not by the status: every
client.vocabulary refusal (any 4xx except 404 and 412, the vocabulary 503s and the
activation 500 materialized_vocabulary_drift) is one, whatever its status. See the
Governed Vocabulary guide.
Catching errors
Section titled “Catching errors”Catch all SDK errors
Section titled “Catch all SDK errors”Every error thrown by the SDK extends ReasoningLayerError:
import { ReasoningLayerError } from '@kortexya/reasoninglayer';
try { await client.sorts.getSort(sortId);} catch (error) { if (error instanceof ReasoningLayerError) { console.error('SDK error:', error.message); }}Catch specific HTTP errors
Section titled “Catch specific HTTP errors”Each HTTP status code has a dedicated error class:
import { ApiError, AuthenticationError, ForbiddenError, BadRequestError, NotFoundError, ConstraintViolationError, RateLimitError, InternalServerError,} from '@kortexya/reasoninglayer';
try { await client.terms.createTerm(request);} catch (error) { if (error instanceof NotFoundError) { console.error('Sort not found:', error.message); // error.status === 404 } else if (error instanceof BadRequestError) { console.error('Invalid request:', error.message); // error.status === 400 // error.body contains the parsed response body } else if (error instanceof ConstraintViolationError) { console.error('Constraint violated:', error.message); // error.status === 409 // error.termId — the term that violated the constraint // error.feature — the feature that violated the constraint // error.constraint — description of the violated constraint } else if (error instanceof AuthenticationError) { console.error('Not authenticated:', error.message); // error.status === 401 } else if (error instanceof ForbiddenError) { console.error('Not authorized:', error.message); // error.status === 403 } else if (error instanceof RateLimitError) { console.error('Rate limited:', error.message); // error.retryAfter — seconds to wait (from Retry-After header) // error.limit — max requests in the current window // error.remaining — requests remaining } else if (error instanceof InternalServerError) { console.error('Server error:', error.status, error.message); } else if (error instanceof ApiError) { // Catch-all for other HTTP errors (e.g., 422) console.error(`HTTP ${error.status}:`, error.message); console.error('Error code:', error.errorCode); console.error('Response body:', error.body); }}Catch non-HTTP errors
Section titled “Catch non-HTTP errors”import { TimeoutError, ValidationError, NetworkError } from '@kortexya/reasoninglayer';
try { await client.inference.backwardChain(request);} catch (error) { if (error instanceof TimeoutError) { console.error(`Request timed out after ${error.timeoutMs}ms`); } else if (error instanceof ValidationError) { console.error(`Validation failed: ${error.message}`); if (error.field) { console.error(` Field: ${error.field}`); } } else if (error instanceof NetworkError) { console.error('Network failure:', error.message); // error.cause may contain the original fetch error }}ApiError properties
Section titled “ApiError properties”All HTTP errors (subclasses of ApiError) carry these properties:
| Property | Type | Description |
|---|---|---|
status |
number |
HTTP status code |
errorCode |
string | undefined |
Backend error code from response body error field |
body |
unknown |
Parsed response body (JSON) |
headers |
Headers |
Response headers |
message |
string |
Human-readable error message |
ConstraintViolationError
Section titled “ConstraintViolationError”The 409 error carries structured information about which constraint was violated:
try { await client.terms.createTerm(request);} catch (error) { if (error instanceof ConstraintViolationError) { console.error('Term ID:', error.termId); console.error('Feature:', error.feature); console.error('Constraint:', error.constraint); // Read from the flat 409 body (ConstraintViolationResponse): term_id, feature, constraint }}A coded 409 refusal (term_has_referrers, term_named_by_rules, …) is read with termRefusalOf(error).
Automatic retry
Section titled “Automatic retry”The SDK automatically retries requests on certain status codes with exponential backoff and jitter.
Default retry behavior
Section titled “Default retry behavior”| Condition | Retried? | Notes |
|---|---|---|
| HTTP 429 | Always | Respects Retry-After header when present |
| HTTP 503 | Optional | Controlled by retryOn503 in ClientConfig |
| Other 5xx | Never | Only 503 is retried when enabled |
| HTTP 4xx | Never | Client errors are not retried |
| Network error | Never | Connection failures are not retried |
| Timeout | Never | Timeouts are not retried |
Configuration
Section titled “Configuration”const client = new ReasoningLayerClient({ baseUrl: 'https://platform.ovh.reasoninglayer.ai', tenantId: 'your-tenant-uuid', auth: { mode: 'cookie' }, maxRetries: 3, // Maximum retry attempts (default: 3) retryOn503: true, // Also retry on 503 Service Unavailable (default: false)});When the server responds with a Retry-After header, the SDK waits that many seconds before retrying. When the header is absent, the SDK uses exponential backoff with jitter.
After all retries are exhausted, the SDK throws the appropriate error (e.g., RateLimitError for 429).
Rate limit headers
Section titled “Rate limit headers”The backend sends rate limit information in response headers:
| Header | Description |
|---|---|
Retry-After |
Seconds to wait before retrying (on 429) |
X-RateLimit-Limit |
Maximum requests allowed in the current window |
X-RateLimit-Remaining |
Requests remaining in the current window |
The RateLimitError class exposes these values:
try { await client.sorts.listSorts();} catch (error) { if (error instanceof RateLimitError) { console.log(`Wait ${error.retryAfter} seconds`); console.log(`Limit: ${error.limit}, Remaining: ${error.remaining}`); }}Timeout configuration
Section titled “Timeout configuration”Client-level default
Section titled “Client-level default”const client = new ReasoningLayerClient({ baseUrl: 'https://platform.ovh.reasoninglayer.ai', tenantId: 'your-tenant-uuid', auth: { mode: 'cookie' }, timeoutMs: 15000, // 15 second default timeout (default: 30000)});Server-side timeouts
Section titled “Server-side timeouts”Some inference endpoints accept a timeoutMs field on the request body. This is a server-side timeout that limits how long the backend spends on proof search. When the server timeout fires, the backend returns whatever results have been found so far (partial results), rather than throwing an error:
const result = await client.inference.backwardChain({ goal: psi('complex_query', { x: '?X', }), timeoutMs: 5000, // Server stops searching after 5 seconds});
// result.solutions may be partial if the server timeout firedThe client-level timeoutMs (set on ClientConfig) controls when the HTTP request is aborted. Set the client-level timeout higher than the server-side timeoutMs on the request body to ensure you receive partial results rather than a client timeout error.