Everything @gainratio/errors exports, with examples. For how classify decides
and why, see Architecture. All examples run against v0.4.0,
the current version on npm.
import { corePack, defineErrorsWith } from "@gainratio/errors";
const errors = defineErrorsWith({}, corePack);
// A raw failure from anywhere: fetch, an SDK, a thrown DOMException.
const code = errors.classify({ status: 429, message: "Slow down" });
console.log(code); // "http.rate_limited"
console.log(errors.describe(code)); // "Too many requests. Wait a moment and try again."For the full loop (register, classify, show it in two languages, send it as
JSON), clone the repo and run pnpm demo. It builds the package and runs
examples/quickstart.mjs.
import { corePack, defineErrorsWith } from "@gainratio/errors";
// Start from the 10 everyday codes and add your own on top.
export const errors = defineErrorsWith(
{},
{
...corePack,
"shop.out_of_stock": {
category: "provider",
params: ["sku"], // the typed contract for this code's params
en: "Sorry, {sku} is sold out.",
},
},
);
errors.describe("shop.out_of_stock", { sku: "MUG-12" });
// "Sorry, MUG-12 is sold out."Pass packs as separate arguments (defineErrorsWith({}, corePack, aiPack)) to
get a startup error if two of them define the same code. Spread them into one
object to get the richest param typing.
try {
await callApi();
} catch (err) {
const code = errors.classify(err); // e.g. { status: 429 } -> "http.rate_limited"
showError(errors.describe(code, {}, t)); // shown through YOUR i18next `t`
}describe calls t("errors.<code>", params). If that key is not translated
yet, it falls back to the catalog's English, so a raw key never reaches the
screen. t can be anything shaped like t(key, params):
const es: Record<string, string> = {
"errors.http.rate_limited": "Demasiadas solicitudes.",
};
const t = (key: string) => es[key] ?? key;
errors.describe("http.rate_limited", {}, t); // "Demasiadas solicitudes."
errors.describe("http.not_found", {}, t); // "That was not found on the server." (English fallback)Set i18nKey on a catalog entry to use a different translation key.
The package ships Spanish (es) and Brazilian Portuguese (pt) for all 24 built-in
codes in corePack, aiPack, bundlePack, and starterPack. They are plain
i18next JSON, nested the way i18next reads keys by default (errors → http →
rate_limited), with i18next's {{name}} placeholders:
import es from "@gainratio/errors/locales/es.json" with { type: "json" };
import pt from "@gainratio/errors/locales/pt.json" with { type: "json" };
i18next.addResourceBundle("es", "translation", es, true, false);
i18next.addResourceBundle("pt", "translation", pt, true, false);
errors.describe("http.rate_limited", {}, i18next.t);
// es: "Demasiadas solicitudes. Espera un momento e inténtalo de nuevo."- Which namespace.
describeasks forerrors.<code>in your default namespace. If your error strings live in another namespace, load the files there and seti18nKey: "myns:errors.http.rate_limited"on the entry, or wraptso it adds the namespace. - Flat keys. If you set
keySeparator: false, i18next wants flat keys such as"errors.http.rate_limited". Flatten the files once when you load them. - Turn off HTML escaping. Set
interpolation: { escapeValue: false }.describereturns plain text, like its English, and you escape it where you render it. With i18next's default, values come back HTML-escaped (/becomes/), in the screen text and in a Problem Detailstitle. - Which language. i18next picks it, from
lngandfallbackLng. A regional language likept-BRreads theptfile, and a language with no file falls back to English. The package itself never picks a language. Theptfile is Brazilian Portuguese, sopt-PTreaders get Brazilian wording. - No English file. English comes from each entry's
en, so a registry built fromstarterPackkeeps its own English wording. - Your own codes. Add
errors.<code>keys to your translation files, or seti18nKeyto reuse a key you already have. A key you have not translated yet shows the English sentence, never the raw key.
A server that answers in the caller's language passes the translated sentence as
the title. The type and every param stay the same, so clients can still
match on the code:
const t = i18next.getFixedT(userLanguage); // e.g. "pt-BR", from your own negotiation
errors.toProblemDetails(code, params, { title: errors.describe(code, params, t) });errors.toProblemDetails("http.rate_limited", { retryAfter: 30 });
// => { retryAfter: 30,
// type: "http.rate_limited",
// title: "Too many requests. Wait a moment and try again.",
// status: 429 }status comes from the entry's first httpStatus. Pass { status } or
{ instance } in the third argument to set them. Params become public fields in
the body, so never pass a secret. Reserved and unsafe names are dropped; the
full list is in Architecture.
import { CanonicalError } from "@gainratio/errors";
// You know what happened, so there is nothing to classify.
throw errors.create("config.missing", { field: "STRIPE_KEY" });
// or without a registry:
new CanonicalError("config.missing", "config", { field: "STRIPE_KEY" });A CanonicalError carries code, params, and category. Its message is
the code itself ("config.missing"), not the English sentence. Call
errors.describe(err.code, err.params) when you need the sentence.
classify returns the fallback code when nothing else matches. It is
internal.unknown (exported as DEFAULT_FALLBACK_CODE) unless you set one:
import { defineErrorsWith } from "@gainratio/errors";
export const errors = defineErrorsWith({ fallbackCode: "shop.unknown" }, shopCodes);
errors.classify({ weird: true }); // "shop.unknown"defineErrorsWith checks that the fallback code is in your catalog. If not, it
throws UnregisteredFallbackError at startup. This also means aiPack on its
own throws, because it does not include internal.unknown. The error says so and
names both fixes: add corePack, or set a fallbackCode that aiPack has:
defineErrorsWith({ fallbackCode: "ai.provider.server_error" }, aiPack);defineErrors(...fragments) is the older entry point. It always falls back to
internal.unknown and does not check it.
A broad match rule registered first can claim failures meant for a later,
more specific rule. priority fixes that. It defaults to 0. Higher runs
earlier, and negative runs last.
const errors = defineErrorsWith(
{},
{
...corePack,
"db.query.timeout": {
category: "timeout",
priority: 10, // beats any default-priority rule, wherever it was registered
match: (raw) => /db .*timeout/i.test(String((raw as Error)?.message ?? "")),
en: "The database query timed out.",
},
},
);To write your own match rules, use the exported helpers errorNameOf,
errorTextOf, and httpStatusOf. They read those fields from any thrown value
without crashing on odd inputs.
category is how your UI and telemetry decide what to do: retry, open
settings, free up space. Seven are built in and exported as KnownCategory:
provider · config · network · timeout · device · integrity ·
internal
You can use any other string too. Category is
KnownCategory | (string & Record<never, never>), so the seven still
autocomplete in an editor, and auth, validation, or billing type-check
without patching this library.
const errors = defineErrorsWith(
{},
{
...corePack,
"auth.session_expired": {
category: "auth", // not one of the seven, still valid
en: "Your session expired. Sign in again.",
},
},
);There are no environment variables and no config files. The only registry-wide
setting is fallbackCode, passed as the first argument to defineErrorsWith.
Everything else is your catalog.
| Export | Kind | What it does |
|---|---|---|
defineErrors |
fn | Registers one or more catalog fragments and returns a typed Registry |
defineErrorsWith |
fn | defineErrors plus RegistryOptions (today: a checked fallbackCode) |
DEFAULT_FALLBACK_CODE |
const | "internal.unknown", the fallback when you set none |
UnregisteredFallbackError |
class | Thrown when the fallbackCode is not in the catalog |
corePack |
const | 10 everyday codes, the pack to start from |
aiPack |
const | 9 ai.* codes, for apps that call an AI model provider |
bundlePack |
const | 5 bundle.* codes, for apps that download a file to the device |
starterPack |
const | The original 18 codes, frozen for the repos that vendor them |
CanonicalError |
class | Error subclass carrying { code, params, category } |
DuplicateCodeError |
class | Thrown when a code is defined in two fragments |
InvalidCatalogEntryError |
class | Thrown when an entry has the wrong shape, e.g. httpStatus: 408 instead of [408] |
errorNameOf / errorTextOf / httpStatusOf |
fn | Helpers for writing your own match rules |
@gainratio/errors/locales/es.json, pt.json |
data | Spanish and Portuguese for every built-in code, as i18next JSON |
Registry methods: classify(raw), describe(code, params?, t?) (t is any
Translator, including i18next's t as-is),
toProblemDetails(code, params?, opts?), problemFor(raw, params?, opts?), create(code, params?), plus
codes, has, and get.
CatalogEntry fields: category (required), en, params, i18nKey,
httpStatus, httpStatusRange, problemType, match, priority.
Types: Catalog, CatalogEntry, Category, KnownCategory, ErrorCode,
MatchRule, Params, ParamsFor, ParamValue, ProblemDetails,
ProblemOptions, Registry, RegistryOptions, TFunction, Translator.