HighX
Server

Errors

The codes, which are retried, and what a client should branch on.

Throw HighXError for anything a client should act on:

import { HighXError } from "@highx/core";

throw new HighXError("conflict", `${existing["editedBy"]} changed this first`);

A plain Error is reported to the client as "an unexpected error occurred" and logged in full server-side. That is right for a bug and wrong for an outcome the interface has to explain.

The codes

CodeMeaningRetried
unauthorizedNobody is signed in. Signing in would helpno
forbiddenWe know who you are, and the answer is nono
invalid_argumentThe arguments failed their schemano
not_foundNo such function, row, or subscriptionno
conflictThe write was based on a version that has moved onyes
rate_limitedToo much, too fastyes
timeoutThe deadline passedyes
unavailableA dependency was downyes
internalA bug, or an unclassified failureno

Only genuinely transient failures are retried. Everything else — including an unclassified error — is treated as permanent, because retrying an unknown failure risks repeating a non-idempotent effect.

Branch on the code, never the message

try {
  await save(changes);
} catch (error) {
  if (isConflictError(error)) offerAChoice();
}

The code is a contract. The message is prose written to be read by a person, and will be reworded.

Dead letters

Work that never succeeds is parked in a dead-letter store with the payload needed to replay it. Failing background work is never dropped silently.

Logging

Errors are logged once, at the boundary that handles them — not at every layer they pass through:

const request = logger.child({ requestId, tenantId });
request.info("mutation committed", { functionPath: "notes.save" });

Redaction matches on field name, recursively, because a secret is not recognisable by its shape. Names are normalized first, so sessionToken, session_token, SESSION-TOKEN, and session.token all match.

A separator-anchored redaction pattern misses camelCase — sessionToken has no boundary between the two words, and without normalization its value reaches the log in full.

On this page