Errors
The four error classes, what the server puts in cause, and the two events that let you react without wrapping every call.
Every failed request throws. The thrown value is always an ApiError or one of its three subclasses, so a
single catch can tell what happened from the class and the status.
The hierarchy
Prop
Type
instanceof is reliable on all four, including after transpilation below ES2015 — the base class restores its
prototype explicitly, which is otherwise the classic way a custom Error subclass stops being recognised.
What the server said: cause
A Sinequa error response carries an envelope with a machine-readable code. When present, it lands on
error.cause:
try {
await client.api.query.search({ name: "<your-query>", text });
} catch (error) {
if (error instanceof ApiError) {
const cause = error.cause as { errorCode?: number; errorCodeText?: string } | undefined;
console.error(error.status, error.message, cause?.errorCode, cause?.errorCodeText);
}
}This matters because the message alone is often not actionable while the code is. The canonical example:
errorCode 6 with "The JSON method 'webtoken' is not allowed by the access token" means the access token's
own rules must be widened — nothing about the client is wrong.
The whole body is read before parsing, so a large envelope is not truncated, and the code survives even on a SharePoint transport where the response is synthetic.
Reacting without wrapping every call
Two events fire before the error is thrown, so an application can surface failures in one place instead of at every call site:
| Event | Fires for | Payload |
|---|---|---|
requestFailed | Any failed request, 401 included | { endpoint, status, error } |
unauthorized | The 401 in particular | { endpoint, error } |
import { ApiError, ServerError, TimeoutError, createAtomicClient } from "@sinequa/atomic";
const client = createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
});
/**
* Subscribe once, at start-up. The returned function unsubscribes — keep it if the
* client outlives the subscriber, otherwise the handler leaks with it.
*/
const stop = client.auth.on("requestFailed", ({ endpoint, status, error }) => {
// The endpoint is the URL the request was actually sent to, not response.url —
// which is empty whenever the response did not come from a real fetch.
console.error(`[${status}] ${endpoint}: ${error.message}`);
if (error instanceof TimeoutError) notify("The server is not responding.");
else if (error instanceof ServerError) notify("Something went wrong on the server.");
else if (error instanceof ApiError && error.status === 403) notify("You cannot see this.");
});
// Observing a 401 is separate from recovering from one — see Handling a 401.
client.auth.on("unauthorized", ({ endpoint }) => {
console.warn("no valid session for", endpoint);
});
await client.initializeConfig();
await client.auth.login();
function notify(message: string) {
console.log(message);
}
// On teardown:
stop();import { DestroyRef, inject, provideEnvironmentInitializer } from "@angular/core";
import { ApiError, ServerError, TimeoutError } from "@sinequa/atomic";
import { SINEQUA } from "./sinequa";
/**
* Registers one failure reporter for the whole application, and unsubscribes with
* the injector — so a hot reload does not stack duplicate handlers.
*/
export function provideErrorReporting() {
return provideEnvironmentInitializer(() => {
const client = inject(SINEQUA);
const destroyRef = inject(DestroyRef);
const stop = client.auth.on("requestFailed", ({ endpoint, status, error }) => {
console.error(`[${status}] ${endpoint}: ${error.message}`);
if (error instanceof TimeoutError) notify("The server is not responding.");
else if (error instanceof ServerError) notify("Something went wrong on the server.");
else if (error instanceof ApiError && error.status === 403) notify("You cannot see this.");
});
destroyRef.onDestroy(stop);
});
}
function notify(message: string) {
console.log(message);
}import { ApiError, ServerError, TimeoutError } from "@sinequa/atomic";
import { useEffect } from "react";
import { sinequa } from "./sinequa";
/**
* Mount once, high in the tree. The subscription is torn down with the component,
* so a remount does not double-report.
*/
export function useErrorReporting(notify: (message: string) => void) {
useEffect(() => {
const stop = sinequa.auth.on("requestFailed", ({ endpoint, status, error }) => {
console.error(`[${status}] ${endpoint}: ${error.message}`);
if (error instanceof TimeoutError) notify("The server is not responding.");
else if (error instanceof ServerError) notify("Something went wrong on the server.");
else if (error instanceof ApiError && error.status === 403) notify("You cannot see this.");
});
return stop;
}, [notify]);
}Timeouts
Requests made by the authentication layer carry a timeout, auth.timeoutMs, defaulting to 10 seconds. They
reject with a TimeoutError.
Ordinary requests have no timeout of their own. Set one per request with an AbortSignal:
import { createTimeoutController } from "@sinequa/atomic";
const { signal } = createTimeoutController(5_000);
// `query.search` takes no request options, so a per-request signal goes through the
// raw HTTP layer. Note what you give up: `client.api.query.search` also post-processes
// the response — pagination, the aggregations' $hasMore flag, tree paths.
const result = await client.http.post("api/v1/query", { name: "<your-query>", text }, { signal });See The HTTP layer for RequestOptions in full.
Failures that are not exceptions
A search result can carry its failure instead of throwing it: result.$error. That happens where the library
recovers a result object rather than letting the rejection through, and it is why an empty result must be
checked before being reported as "no matches". See Result anatomy.