Skip to content

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.

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.

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);
}
}

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);
}
}
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
}
}

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

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).

The SDK automatically retries requests on certain status codes with exponential backoff and jitter.

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
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).

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}`);
}
}
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)
});

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 fired

The 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.