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
| Code | Meaning | Retried |
|---|---|---|
unauthorized | Nobody is signed in. Signing in would help | no |
forbidden | We know who you are, and the answer is no | no |
invalid_argument | The arguments failed their schema | no |
not_found | No such function, row, or subscription | no |
conflict | The write was based on a version that has moved on | yes |
rate_limited | Too much, too fast | yes |
timeout | The deadline passed | yes |
unavailable | A dependency was down | yes |
internal | A bug, or an unclassified failure | no |
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.