@sinequa/atomic

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

Error ApiErrorstatus: number UnauthorizedError401 ServerError500 TimeoutError408

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:

reading the server's own code
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:

EventFires forPayload
requestFailedAny failed request, 401 included{ endpoint, status, error }
unauthorizedThe 401 in particular{ endpoint, error }
One place to report failures
src/error-reporting.ts
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();

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:

a timeout for one request
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.

Pitfalls

What's next

On this page