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.

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)
│ ├── RateLimitError (429: .retryAfter, .limit, .remaining)
│ └── InternalServerError (500+)
├── TimeoutError (.timeoutMs)
├── ValidationError (.field)
└── NetworkError

Catching errors

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

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

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

All HTTP errors (subclasses of ApiError) carry these properties:

PropertyTypeDescription
statusnumberHTTP status code
errorCodestring | undefinedBackend error code from response body error field
bodyunknownParsed response body (JSON)
headersHeadersResponse headers
messagestringHuman-readable error message

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);
// These are parsed from the response body's `details` field
}
}

Automatic retry

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

Default retry behavior

ConditionRetried?Notes
HTTP 429AlwaysRespects Retry-After header when present
HTTP 503OptionalControlled by retryOn503 in ClientConfig
Other 5xxNeverOnly 503 is retried when enabled
HTTP 4xxNeverClient errors are not retried
Network errorNeverConnection failures are not retried
TimeoutNeverTimeouts are not retried

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

The backend sends rate limit information in response headers:

HeaderDescription
Retry-AfterSeconds to wait before retrying (on 429)
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests 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

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

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.