# Error codes

Every E_ code a Comvi i18n production build can emit, plus the @comvi/next and @comvi/nuxt server errors and the capability-missing pair: what each one means, where it comes from, and how it reaches you.

Canonical: https://comvi.io/docs/i18n/errors

Branch on the `E_…` code an error carries, report the rest by message, and look each code up
here. Every development message starts with the package that produced it: `[@comvi/core]` for the
instance and both compilers, `[@comvi/plugin-fetch-loader]` for the loader, and `[@comvi/react]`,
`[@comvi/vue]`, `[@comvi/solid]`, `[@comvi/svelte]`, `[@comvi/next]` or `[@comvi/nuxt]` for a
binding's own checks. A production build of the instance throws the bare `E_…` token instead of
the sentence, with no prefix.

`src/i18n.ts`

```ts

// Your error tracker: Sentry, Rollbar, a POST to your own endpoint.
declare function reportToSentry(error: unknown, context: unknown): void;

const i18n = createI18n({
locale: 'en',
onError(error, context) {
    // E_ICU_SYNTAX is the one reported error with a `.code`; the two E_USE_* codes
    // are thrown synchronously from .use() and never reach this handler.
    const code = (error as { code?: string }).code;
    if (code === 'E_ICU_SYNTAX') {
      // context: { source: "compile", key, namespace, locale }
      reportToSentry(error, { ...context, code });
      return;
    }
    console.error(`${context?.source}: ${error.message}`, context);
},
});
```

Three codes carry a `code` own-property, so `catch` can branch without matching text:
`E_ICU_SYNTAX`, `E_USE_INSTANCE_IDENTITY` and `E_USE_NOT_INSTALLER`. Everything else is matched by
message or allowed to propagate. `E_ICU_SYNTAX` carries a second own-property, `argumentType`:
`"plural"`, `"select"`, `"selectordinal"`, or the parsed token such as `"number"` or `"date"`.

### How it works

An `E_…` token is a production error identity, not a message. Each one is written in the source
as `IS_DEV ? "a long explanatory sentence" : "E_SOMETHING"`, and the `development` export
condition picks the arm at build time. A development build throws the sentence and the token
never appears; a production build throws the token and the sentence is not in the bundle. Every
code has a distinct development sentence; six are quoted in both arms and the rest are
summarized in [the table](#every-code-a-production-build-can-emit). The capability-missing pair
and the `@comvi/next` and `@comvi/nuxt` server errors are not build-conditional and throw the
same text in both builds.

## Codes quoted in both builds

### `E_ICU_SYNTAX`

**Development:** one of five sentences, depending on the compiler and the failure. On the default compiler, for a plural or select the ICU compiler would handle: `[@comvi/core] "{…}" in "…" is ICU "plural" syntax; the default compiler supports {param} only. Pass compiler: icuCompiler from "@comvi/core/icu" in the constructor options.` On the same compiler, for anything else: `[@comvi/core] "{…}" in "…" is ICU "number" syntax; the default compiler supports {param} only. "number" is not a shipped ICU argument type: quote the literal ('{…}') or pass your own compiler.` On the ICU compiler, for an argument it does not implement: `` [@comvi/core] ICU argument `{…}` is not supported: this compiler implements `plural`, `selectordinal` and `select`, each with a choice list like `other {…}`. Quote the braces ('{'…'}') or format the value in code and pass it as a plain {param} ``. For a choice list it cannot parse: `` [@comvi/core] The `plural` choice list `…` is malformed; every selector needs a braced sub-message, as in `other {…}` ``. For an exact selector that is not a number: `` [@comvi/core] The exact selector `=one` in the `plural` choice list `…` is not a number; an ICU exact selector is `=` followed by a number, as in `=1 {…}` ``.

**Production:** `E_ICU_SYNTAX`, with `code` and `argumentType` own-properties. Production does not throw at ingestion: the braced segment renders literally and the error is reported once through `onError` with `source: 'compile'`, or printed with `console.error` when no handler is set. Development walks every string as the catalog is ingested.

**Cause:** a catalog uses ICU the active compiler cannot compile. **Fix:** for `plural`, `select` and `selectordinal`, pass `compiler: icuCompiler` from `@comvi/core/icu`. For `number`, `date` and `time`, which neither compiler implements, format the value in code and interpolate it as a plain `{param}`, or quote the braces. The plural `offset:` form reads as a selector with no braced sub-message, so it produces the malformed choice list message; drop the offset and do the arithmetic in your own parameter. See [ICU reference](/docs/i18n/icu-reference#what-icu-does-not-implement).

### `E_USE_NOT_INSTALLER`

**Development:** `[@comvi/core] .use(f) expects an installer that returns the i18n instance it was given, but f returned …. A raw plugin goes through .use(definePlugin(f)) or registerPlugin(f); a plugin package's installer is the name WITHOUT the Plugin suffix (localeDetector(), not localeDetectorPlugin()).` **Production:** `E_USE_NOT_INSTALLER`, with `code`.

**Cause:** `.use(f)` was handed a function that returned something other than the i18n instance, which is what a raw plugin, an `async` installer and a `Plugin`-suffixed factory all do. **Fix:** wrap your own plugin in `definePlugin(fn)`, or call the package's bare-name installer.

### `E_INSTALLER_AS_PLUGIN`

**Development:**

```text
[@comvi/core] localeDetector() is a .use installer, not a plugin, and it ran during plugin initialization — it was registered with registerPlugin(localeDetector(…)) or definePlugin(localeDetector(…)). Compose it instead: createI18n(…).use(localeDetector(…)). Nothing was installed.
```

**Production:** `E_INSTALLER_AS_PLUGIN`.

**Cause:** an installer went to `registerPlugin()` or `definePlugin()` instead of `.use()`. **Fix:** compose it: `createI18n(…).use(installer(…))`. It throws before anything attaches, so the instance is unchanged.

### `E_USE_INSTANCE_IDENTITY`

**Development:** `[@comvi/vue] .use(installer) must return the instance it was given.` The Vue text continues `A raw plugin goes through .use(definePlugin(f)) or registerPlugin(f); …`, and the Next.js text opens with `[@comvi/next]` and offers `.use(nextPlugin(f, { runtime }))` instead of `registerPlugin(f)`. **Production:** `E_USE_INSTANCE_IDENTITY`, with `code`.

**Cause:** an installer handed to a wrapper's `.use()` returned a different object than the core instance it was given, which would leave the wrapper's bound proxies driving an instance your app never sees. **Fix:** widen the instance in place and return that same instance. Raised by `@comvi/vue` and `@comvi/next`.

### `E_PLUGIN_INIT_RETURN`

**Development:** ``[@comvi/core] A plugin returned a value. A plugin may only return nothing (`undefined`) or a cleanup function, so nothing was registered. If this was a bare-name installer, compose it with `.use(installer(…))` … If it was your own plugin, use a statement body: `(i18n) => { flag = true; }`.`` **Production:** `E_PLUGIN_INIT_RETURN`.

**Cause:** a plugin returned something that is neither `undefined` nor a cleanup function, usually an expression-bodied arrow leaking its last value. **Fix:** use a statement body.

### `E_LOADER_IMPORT_MAP`

**Development:**

```text
[@comvi/core] loader(importMap) requires the import map. A static map of import functions goes here: .use(loader({ uk: () => import('./uk.json') })). A plain loader FUNCTION goes to .use(attachLoader) followed by i18n.registerLoader(fn) — both from @comvi/core/loader.
```

**Production:** `E_LOADER_IMPORT_MAP`.

**Cause:** `loader()` was called with no argument. TypeScript rejects that first, so it is reachable only from JavaScript. **Fix:** pass the map, or use `attachLoader` plus `registerLoader(fn)` for a plain loader function.

## A capability was never composed

These two are not `E_` codes: they carry no `code` own-property and are thrown in both build conditions at the acquisition call.

```text
// loader, development
[@comvi/core] This i18n instance has no loader capability. Compose it: .use(attachLoader) from "@comvi/core/loader", or loader(map).
// loader, production
[@comvi/core] missing loader capability — attach @comvi/core/loader
// plugins, development
[@comvi/core] This i18n instance has no plugins capability. Compose it: .use(plugins()) from "@comvi/core/plugins", or the lower-level attachPlugins.
// plugins, production
[@comvi/core] missing plugins capability — attach @comvi/core/plugins
```

The same omission also shows up as "is not a function" failures. For the loader members there is a development-only exception: `attachPlugins` installs a marked stand-in for each of them, so `reloadMessages()` on an instance with only `plugins()` composed raises the capability message instead of a bare `TypeError`. Nothing stands in for a missing plugin capability, so `onMissingKey` is a bare `TypeError` in both builds.

| symptom | fix |
| --- | --- |
| `addActiveNamespace` / `removeActiveNamespace` / `reloadMessages` / `onLoadError` is not a function | `useI18nLoader()` |
| `onMissingKey is not a function` | `useI18nPlugins()` |
| vue: `i18n.registerLoader` / `i18n.registerPlugin` is not a function | `i18n.core.*` |
| vue: `i18n.core.reloadMessages` will not type-check | `core` is typed as the bare instance; use `useI18nLoader()` |

## `@comvi/next` server errors

The thrown messages have no `IS_DEV` branch, so the text is the same in both builds; the two warnings at the end are development only.

**`[@comvi/next] Locale not set. Call setRequestLocale(locale) in your layout/page first, or configure middleware.`** **Cause:** `getI18n()` ran without an explicit `locale`, and nothing had named one: neither `setRequestLocale()` filling the per-request cache nor the middleware setting the `x-comvi-locale` header. A request that cannot name a locale never triggers `init()`. **Fix:** pass the locale, `getI18n({ locale })`; or render under `<ComviProvider locale>`, call `setRequestLocale(locale)` first, or configure the middleware. Calling `getLocale()` directly gives `[@comvi/next] Unable to determine locale. Make sure to call setRequestLocale() in your layout/page or configure middleware.`

```text
[@comvi/next] i18n already configured by createNextI18nFromInstance(); setI18n() is a second source. Configure it once — only a same-instance setI18n() repeats.
```

Either source can appear on either side of the sentence. **Cause:** server i18n was configured twice from different sources, or `setI18n()` was called twice with different instances. A repeat call with the same instance is a no-op, because setup files commonly run twice. **Fix:** configure once. If it appears in development after editing the setup module, restart the dev server. When nothing configured it at all:

```text
[@comvi/next] i18n not configured. Add withComvi({ config }) to next.config, or call setI18n(i18n) / createNextI18nFromInstance(factory, routing). In a test runner the alias does not apply: alias `@comvi/next/config` to your config module (vitest `resolve.alias`, jest `moduleNameMapper`) or call setI18n(i18n) in test setup. `serverExternalPackages` containing @comvi/next is not supported.
```

```text
[@comvi/next] <ComviProvider> needs withComvi({ config }) in next.config: the client reads the i18n instance from that module. Add it, or keep your own "use client" wrapper around <I18nProvider> from @comvi/next/client (the manual setup, also the one for a config whose loader needs server-only code).
```

**Cause:** the layout renders the server `<ComviProvider>` but `next.config` does not export through `withComvi`, so the client graph has no instance to hand it. The client half of the same check, a client graph built without the alias, throws the first sentence alone: `[@comvi/next] <ComviProvider> needs withComvi({ config }) in next.config: the client reads the i18n instance from that module.` **Fix:** add the plugin and restart the dev server, or stay on the manual setup with your own client wrapper.

`withComvi` itself refuses a path it cannot find while `next.config` loads: `[@comvi/next] withComvi: the config module <absolute path> does not exist (config: "<what you wrote>", resolved against <cwd>).`, followed by the `process.cwd()` rule and the absolute-path form.

**`[@comvi/next] Two i18n instances: one from setI18n(), another from the withComvi({ config }) module. Using the first; keep one source.`** Development only, once. **Cause:** a `setI18n()` or `createNextI18nFromInstance()` registration and the `withComvi` module export different objects. **Fix:** delete the registration, or point `withComvi` at the module that builds the registered instance.

## `@comvi/nuxt` errors

The module's server utilities have no `IS_DEV` branch either. The fresh-instance guard is the one
development-only check, emitted into the module's generated code and absent from production builds.

### `[@comvi/nuxt] i18n not configured`

``[@comvi/nuxt] i18n not configured. Add "@comvi/nuxt" to `modules` in nuxt.config.ts.`` **Cause:** `useI18n()` ran in an app whose `nuxt.config.ts` does not list the module, so no plugin provided the instance. **Fix:** add `'@comvi/nuxt'` to `modules` and restart the dev server.

### ``[@comvi/nuxt] `comvi.<key>` is not an option``

```text
[@comvi/nuxt] `comvi.detectBrowserLanguage` is not an option: use `localeCookie` and `localeDetection`.
[@comvi/nuxt] `comvi.fallbackLanguage` is not an option: use `fallbackLocale`.
[@comvi/nuxt] `runtimeConfig.public.comvi.cookieName` is not an option: use `localeCookie`.
[@comvi/nuxt] `comvi.localeDetection.cookieName` is not an option: use `comvi.localeCookie.name`.
[@comvi/nuxt] `comvi.localeDetection.redirectOnFirstVisit` is not an option.
```

Thrown at build time, one per key, with the option to write instead where one exists; a key under `runtimeConfig.public.comvi` is reported with that prefix. **Cause:** a `comvi` key, or a key inside `localeCookie` or `localeDetection`, that the routing schema shared with `@comvi/next` does not know. **Fix:** write the option the message names; every one is on [Module options](/docs/i18n/api/nuxt#module-options). Two more build-time refusals guard `localeDetection.resolveAcceptLanguage`, which the module copies into the build as source: ``[@comvi/nuxt] `localeDetection.resolveAcceptLanguage` must be a function.`` and ``[@comvi/nuxt] `localeDetection.resolveAcceptLanguage` could not be copied into the build. Write it as a self-contained function expression: it cannot be a bound or native function.``

### `[@comvi/nuxt] comvi i18nModule returned an instance it already returned`

```text
[@comvi/nuxt] comvi i18nModule returned an instance it already returned. Every call builds a separate instance (the client, and each server request), so return a new instance from createI18n on every call.
```

**Cause:** the `comvi.i18n.ts` factory handed back an instance the module had already received, which happens when the file builds one instance at module scope and returns it from the function. One shared instance would carry one locale and one catalog across every request. **Fix:** call `createI18n(options)` inside the factory so each call builds its own instance, as on [Nuxt](/docs/i18n/nuxt#load-from-comvi).

### `[@comvi/nuxt] useTranslation()/loadMessages() after the response ended`

```text
[@comvi/nuxt] useTranslation()/loadMessages() after the response ended: the request instance is already destroyed. Translate before responding, or create your own instance.
```

**Cause:** the per-request instance was already torn down. The module destroys every instance a request built once Nitro's `afterResponse` (or `error`) hook fires and the request's `event.waitUntil` background work has settled; a later call is refused rather than handed an instance nothing will tear down. **Fix:** call `useTranslation()` / `loadMessages()` before the handler returns, or build and own your own instance. See [Render on the server](/docs/i18n/ssr) and [Server utilities](/docs/i18n/api/nuxt#server-utilities).

## Type generation

A refusal from `comvi typegen` and `@comvi/vite-plugin`, thrown before either tool writes a file. **Text (first line):** `Generated types have to augment a Comvi module, and <path-to-package.json> declares none.` The rest lists the packages it accepts and mentions the `augment` override.

**Cause:** the app's `package.json` declares neither `@comvi/core` nor a framework binding (`@comvi/react`, `@comvi/next`, `@comvi/vue`, `@comvi/nuxt`, `@comvi/svelte`, `@comvi/solid`). The rule reads the manifest, not `node_modules`, so a transitive install does not count. **Fix:** add the package the app imports `t()` from, or `@comvi/core`, to its dependencies. A private wrapper package can be named with `augment` in `.comvirc.json` or the Vite plugin's `augment` option. See [Generated type structure](/docs/i18n/type-safety#generated-type-structure).

## Every code a production build can emit

Columns: what you did, which part of the library owns the check, the sentence a development build throws in the token's place, and whether the failure reaches you as a **throw** (a rejected promise, for the `async` entry points) or as a **report** through `onError` / `onLoadError`. *Reported, then thrown* means both happen.

| code | happens when | owner | development text | reaches you as |
| --- | --- | --- | --- | --- |
| `E_LOCALE_NOT_SET` | `createI18n()` was called without a `locale` | the i18n instance, constructor | `[@comvi/core] Locale is not set` | thrown synchronously |
| `E_MESSAGES_NOT_OBJECT` | the `messages` option is not a plain object (a production build checks the top level only; development additionally rejects a non-object *value* inside it) | the i18n instance, constructor | `` [@comvi/core] `messages` is not an object `` | thrown synchronously |
| `E_INSTANCE_DESTROYED` | `init()` was called after `destroy()` | the i18n instance, `init()` | `[@comvi/core] Cannot call init() after destroy(). Create a new i18n instance.` | reported (`source: 'init'`), then thrown; `init()` rejects |
| `E_RESERVED_DEFAULT_PARAMS` | `defaultParams` contains a call-control key: `locale`, `ns`, `fallback`, `markers`, `tagInterpolation` | the i18n instance, constructor and `setDefaultParams()` | `[@comvi/core] defaultParams cannot contain call-control keys: locale, ns, fallback, markers, tagInterpolation` | thrown synchronously |
| `E_NULLISH_DEFAULT_PARAMS` | a `defaultParams` value is `null` or `undefined` | the i18n instance, constructor and `setDefaultParams()` | `[@comvi/core] defaultParams values cannot be null or undefined` | thrown synchronously |
| `E_DEFAULT_PARAMS_GUARANTEED_KEY` | `setDefaultParams()` dropped a key the constructor's type guarantees | the i18n instance, `setDefaultParams()` | `[@comvi/core] defaultParams must preserve constructor-guaranteed key "…"` | thrown synchronously |
| `E_ICU_SYNTAX` | a message uses ICU the active compiler does not implement | compiler (`@comvi/core` default, or `@comvi/core/icu`) | one of the five sentences [quoted above](#e_icu_syntax) | thrown at compile time with `.code`; in production the segment renders literally and is **reported** (`source: 'compile'`) |
| `E_MISSING_TAG_HANDLER` | a message contains a tag with no entry in the components map | two producers: the `strict: true` throw in `@comvi/core/tags`, and the constructor's default `onTagWarning` | `` [@comvi/core] Missing handler for tag: <name> `` | depends on `tagInterpolation.strict`: **thrown** on `true`; **reported** (`source: 'translation'`, `tagName` set) on `'warn'` by the default `onTagWarning`; **silent** on the default `false`, where the tag's inner text is rendered |
| `E_LOADER_IMPORT_MAP` | `loader()` was called with no import map | loader capability, `loader()` | [quoted above](#e_loader_import_map) | thrown synchronously |
| `E_NO_LOADER_REGISTERED` | `reloadMessages()` ran with no loader registered | loader capability | `[@comvi/core] No loader registered. Cannot reload messages.` | thrown; the promise rejects |
| `E_REGISTER_LOADER_ARG` | `registerLoader()` was handed something that is not a function | loader capability | `[@comvi/core] registerLoader(): argument must be a loader function. For a static import map use .use(loader(map)), or wrap it with createImportMapLoader(map, () => i18n.getDefaultNamespace())`, then names the subpath both come from; quoted in full on [Troubleshooting](/docs/i18n/troubleshooting) | thrown synchronously |
| `E_REGISTER_LOADER_ENTRY` | the import-map loader has no entry for a `locale:namespace` pair | `createImportMapLoader` / `.use(loader(map))` | `[@comvi/core] registerLoader: no entry for "fr:default"` | thrown out of the loader, so it becomes a namespace-load failure: `loadError` / `onLoadError` |
| `E_PARTIAL_NAMESPACE_LOAD` | some, but not all, namespaces failed for one locale | loader capability | `[@comvi/core] Partial namespace load failure for "fr": 1/2 failed (admin)` | **reported only** (`source: 'namespace-load'`); never thrown, the load resolves |
| `E_ALL_NAMESPACES_FAILED` | every namespace failed for one locale | loader capability | `[@comvi/core] Failed to load all namespaces for locale "fr": default, admin` | reported (`source: 'namespace-load'`), then thrown; rejects `init()`, `setLocale()`, `addActiveNamespace()` |
| `E_FAILED_RELOAD_TRANSLATIONS` | every locale in a `reloadMessages()` call failed | loader capability | `[@comvi/core] Failed to reload messages` | reported (`source: 'namespace-load'`), then thrown |
| `E_PLUGIN_INIT_TIMEOUT` | a plugin's init did not settle inside its `timeout`, 10000 ms unless set | plugins capability | `[@comvi/core] Plugin initialization timed out after 10000ms` | the plugin's own `onError`, then reported (`source: 'plugin'`); rethrown only if the plugin is `required` |
| `E_PLUGIN_INIT_FAILED` | a plugin threw something that is not an `Error` | plugins capability | `[@comvi/core] Plugin initialization failed: …` | same route as the timeout |
| `E_PLUGIN_INIT_RETURN` | a plugin returned a value that is neither `undefined` nor a cleanup function | plugins capability | [quoted above](#e_plugin_init_return) | same route as the timeout |
| `E_INSTALLER_AS_PLUGIN` | an installer was passed to `registerPlugin(…)` or `definePlugin(…)` instead of `.use(…)` | plugins capability, during the plugin drain | [quoted above](#e_installer_as_plugin) | same route as the timeout |
| `E_REGISTER_LOCALE_DETECTOR` | `registerLocaleDetector()` was handed something that is not a function | plugins capability | `[@comvi/core] registerLocaleDetector(): argument must be a function.` | thrown synchronously |
| `E_REGISTER_POST_PROCESSOR` | `registerPostProcessor()` was handed something that is not a function | plugins capability | `[@comvi/core] registerPostProcessor(): argument must be a function. Received: string` | thrown synchronously |
| `E_USE_INSTANCE_IDENTITY` | an installer returned something other than the instance `.use()` handed it | the `@comvi/vue` and `@comvi/next` wrappers, `use()` | [quoted above](#e_use_instance_identity) | thrown synchronously, with `.code` |
| `E_USE_NOT_INSTALLER` | the function handed to `.use()` returned something other than the instance it was given | the i18n instance, `use()` | [quoted above](#e_use_not_installer) | thrown synchronously, with `.code` |

> **Three tokens that never reach you**
>
> `E_TRANSLATION_NOT_FOUND`, `E_INVALID_PLURAL_PARAM` and `E_MISSING_TAG_FALLBACK` sit next to the codes in the table in the source, but they are the production branch of a `warn()` call, and `warn()` compiles out of production builds entirely, so the strings are not in the shipped bundles. In development those three print their explanatory sentence to the console instead. Do not write a handler for them.

## Warnings, not throws

Two `@comvi/next` diagnostics reach the console rather than a `catch`, in both builds. Four more name a locale outside `routing.locales`. Two are development-only: `[@comvi/next] getI18n({ locale: "fr" }): "fr" is not in routing.locales (en, de). Call notFound() for an unknown locale before rendering.`, once per locale, and `[@comvi/next] Ignored the x-comvi-locale header "xx": it is not in routing.locales (en, de).`, once per process when `getLocale()` ignores the request header, ending `it is not a locale code.` when there is no `routing` export. The other two fire in both builds: `[@comvi/next] getPathname: unknown locale "fr". Expected one of: en, de` on every such call, and, through `i18n.reportError`, `[@comvi/next] <I18nProvider>: locale "fr" is not in routing.locales (en, de). Skipping locale sync.`

```text
[@comvi/next] No loader registered, so SSR loaded nothing. Register one via i18n.registerLoader(...), or compose one on the createNextI18n(...) result: .use(cdnLoader({ cdnUrl })) or .use(loader(map)).
```

Warned once per instance when a server helper asks for a namespace and the instance has no loader; nothing throws and nothing loads, so the page renders keys or fallbacks. The second, `[@comvi/next] Failed to load ${locale}:${namespace}:`, precedes the underlying load error for one `locale:namespace` pair, and the render continues without it; the pair is attempted once per request and reported through `loadError`, never thrown.

The `@comvi/nuxt` server utilities warn with the same two stems. With no loader on the instance, once per instance, the message depends on what the instance has:

```text
[@comvi/nuxt] No loader registered, so SSR loaded nothing. This i18n instance has the loader capability: pass the import map where you compose it — createI18n(options).use(loader(map)) in your `i18nModule` factory — or, on an instance composed with .use(attachLoader), register a LoaderFn from comvi.setup: (({ core }) => { core.registerLoader(fn); }).
```

```text
[@comvi/nuxt] This i18n instance has no loader capability, so SSR loaded nothing. The generated default instance is core's base instance — compose the loader in comvi.i18n.ts (or the file `comvi.i18nModule` names): createI18n(options).use(loader(map)), both imported from "@comvi/nuxt/i18n".
```

A pair that fails to load logs `[@comvi/nuxt] Failed to load ${locale}:${namespace}:` before the underlying error, and the call resolves without it.

## Binding checks

Each binding throws one message when a hook or component runs outside its provider; none is build-conditional.

| message | when |
| --- | --- |
| `[@comvi/react] Hooks must be used within an I18nProvider.` | any `@comvi/react` hook but `useLocale` outside the provider |
| `[@comvi/react] useLocale must be used within an I18nProvider.` | `useLocale()` outside the provider |
| `[@comvi/react] Initialization failed:` followed by the rejection | `init()` rejected and the provider has no `onError`; printed with `console.error`, the default |
| `[@comvi/vue] useI18n must be used within a Vue app with i18n plugin installed. Make sure you called app.use(i18n) before using this composable.` | `useI18n()` without `app.use(i18n)` |
| `[@comvi/vue] <T> component must be used within a Vue app with i18n plugin installed` | `<T>` without `app.use(i18n)` |
| `[@comvi/solid] i18n context not found. Wrap your app with <I18nProvider i18n={i18n}>.` | `useI18n()` outside the provider |
| `[@comvi/svelte] i18n context not found. Call setI18nContext(i18n) in your root component (e.g., App.svelte).` | `useI18n()` with no `setI18nContext()` ancestor |
 `<T>` renders a tag handler as a component, so a handler that throws is caught by the tag's error boundary and reported with `source: 'translation'`; there is no "must return an element" check.

## `@comvi/plugin-fetch-loader`

Every message carries `[@comvi/plugin-fetch-loader]` and the same text in both builds. One is thrown synchronously at `.use()`; the rest are load failures, so they reach `onLoadError` and the `loadError` event with `source: 'namespace-load'`, and `init()` or `setLocale()` rejects only when every requested namespace failed.

| message | entry | when |
| --- | --- | --- |
| `cdnUrl is required` | both | the factory was called without `cdnUrl`; thrown at `.use()` |
| `HTTP 404: Not Found` | both | the CDN answered with a status outside 2xx; the status and text are the response's |
| `Request timeout after 10000ms` | both | the request, body included, outran `timeout`; the error's `name` is `TimeoutError` |
| `Request aborted: <url>` | both | `clearMessages()`, `destroy()` or a superseded `setLocale()` cancelled the request; the error's `name` is `AbortError`. On the API path the suffix is `API translations` or `API translation fallback` instead of a URL |
| `Invalid JSON response from <url>` | both | the body did not parse |
| `Invalid CDN response: "fr:default" must be an object` | both | the body parsed to something that is not a catalog object; on the API path it reads `Invalid API response: "namespaces.default.fr" must be an object` |
| `Invalid locale: "<value>". Only alphanumeric, underscore, hyphen, dot, and @ characters are allowed; dot-only path segments are rejected.` | both | a locale or namespace cannot be a URL segment, refused before any request is sent; the label is `locale`, `namespace` or `CDN root namespace` |
| `Failed to fetch project info: 500 Internal Server Error` | `/api` | the project-info request failed after its retries; `404 <url>` when both endpoint spellings answered 404 |
| `Invalid API response from <url>: "id" must be a number or a path-safe string` | `/api` | the project-info body has no usable `id` |
| `API error: 500 Internal Server Error` | `/api` | an export request answered outside 2xx |
| `No translations found for fr:default` | `/api` | neither the API nor the `fallback` map produced a catalog for the pair |

## Errors documented elsewhere

- The context errors of each binding, listed under [Binding checks](#binding-checks), with their fixes on [Troubleshooting](/docs/i18n/troubleshooting).
- **`[@comvi/next] Invalid lazy plugin module. Expected a plugin function or { default: pluginFunction }.`**: thrown by `nextPlugin()` when a lazily imported module is neither a plugin function nor a module whose `default` is one. Import the plugin function itself, or export it as `default`.
- The Comvi REST API has its own codes, returned in an HTTP response body: [Error Reference](/docs/api/errors).

## Read next

- [Handle missing keys and errors](/docs/i18n/error-handling): `onError`, `onMissingKey`, the `loadError` event, and reporting to a tracker.
- [Troubleshooting](/docs/i18n/troubleshooting): symptom to cause to fix for the failures a first run hits.
