Error Handling
Comvi is designed to keep your app rendering even when translations are missing or fail to load. Missing keys do not throw by default: they emit a 'missingKey' event and fall back to a configured value or the key itself. Load, plugin, post-processor, event-listener, and rendering errors are reported through onError or specific events.
Some operations can still fail explicitly. init(), setLocaleAsync(), addActiveNamespace(), and reloadTranslations() return promises and may reject when required setup or namespace loading fails. t() can also throw if your onMissingKey handler throws, or when tag interpolation is configured with tagInterpolation.strict: true.
This guide covers default behavior, error events, fallback strategies, and production logging patterns.
Default Behavior
Section titled “Default Behavior”When a translation key is not found, t() returns the key itself rather than throwing:
const { t } = useI18n();
t('dashboard.title'); // "Dashboard" (found)t('nonexistent.key'); // "nonexistent.key" (missing — key returned as fallback)This keeps your UI readable while still giving you hooks to report the missing key.
Error Events
Section titled “Error Events”Subscribe to specific error types with convenience methods, or use the generic on() event system:
import { createI18n } from '@comvi/core';
const i18n = createI18n({ locale: 'en' });
// Listen for missing translation keysconst unsubscribe = i18n.onMissingKey((key, locale, namespace) => { console.warn(`Missing key: "${key}" in [${locale}/${namespace}]`);});
// Listen for translation load failuresi18n.onLoadError((locale, namespace, error) => { console.error(`Failed to load [${locale}/${namespace}]:`, error);});
// Or use the generic event systemi18n.on('missingKey', ({ key, locale, namespace }) => { console.warn(`Missing: ${key}`);});
i18n.on('loadError', ({ locale, namespace, error }) => { console.error('Load failed:', error);});
// Unsubscribe when no longer neededunsubscribe();Error Reporting
Section titled “Error Reporting”Comvi uses standard JavaScript Error objects paired with an ErrorReportContext that identifies the error source. Configure a global error handler via the onError option:
import { createI18n } from '@comvi/core';
const i18n = createI18n({ locale: 'en', onError: (error, context) => { console.error(`[i18n] ${context?.source}: ${error.message}`, context); },});The context object identifies where the error originated:
interface ErrorReportContext { source: | 'plugin' // Plugin threw during init or execution | 'plugin-cleanup' // Plugin threw during cleanup/destroy | 'init' // i18n.init() failed | 'translation' // Translation rendering error (e.g. ICU parser, tag handler) | 'namespace-load' // Namespace loader failed | 'post-processor' // Post-processor function threw | 'event'; // Event listener threw pluginName?: string; tagName?: string; key?: string; locale?: string; namespace?: string; event?: string;}Common patterns:
source: 'plugin'— a plugin threw during initialization (e.g., Fetch Loader couldn’t reach CDN)source: 'namespace-load'— one or more namespaces failed to load (other namespaces may have succeeded)source: 'post-processor'— a post-processor function threw (error in your custom logic)source: 'translation'— an error during translation rendering (e.g., bad ICU syntax, tag handler error)
Events
Section titled “Events”In addition to the onError handler, Comvi emits specific events you can subscribe to:
| Event | When It Fires | Payload |
|---|---|---|
missingKey | t() cannot find a key in any language | { key, locale, namespace } |
loadError | A namespace loader failed for a locale/namespace pair | { locale, namespace, error } |
localeChanged | Active language was changed | { from, to } |
namespaceLoaded | A namespace finished loading | { namespace, locale } |
loadingStateChanged | Loading state changed | { isLoading, isInitializing } |
Custom Fallback Value
Section titled “Custom Fallback Value”Override the default fallback (key name) with the onMissingKey option. The createI18n() option receives an object with key, locale, and namespace:
const i18n = createI18n({ locale: 'en', onMissingKey: ({ key, locale, namespace }) => { // Return empty string instead of the key return ''; },});const i18n = createI18n({ locale: 'en', onMissingKey: ({ key, locale, namespace }) => { // Return a visual indicator for developers if (import.meta.env.DEV) { return `[MISSING: ${key}]`; } return key; },});Fallback Language Chain
Section titled “Fallback Language Chain”When a key is missing in the active language, Comvi checks the fallback language before reporting an error:
const i18n = createI18n({ locale: 'de', fallbackLocale: 'en', // If key is missing in 'de', try 'en'});You can also pass an array to define a chain of fallback languages:
const i18n = createI18n({ locale: 'de-AT', fallbackLocale: ['de', 'en'], // Try 'de' first, then 'en'});The resolution order is:
- Look up the key in the active language (
de-AT) - If missing, try each fallback language in order (
de, thenen) - If still missing, call
onMissingKeyhandler or return the key
Per-Component Error Handling
Section titled “Per-Component Error Handling”Use onLoadError and onMissingKey from useI18n() to handle errors at the component level. Both return an unsubscribe function you should call on cleanup:
import { useI18n } from '@comvi/react';import { useEffect, useState } from 'react';
function TranslatedSection() { const { t, onLoadError } = useI18n(); const [loadFailed, setLoadFailed] = useState(false);
useEffect(() => { const unsubscribe = onLoadError((locale, namespace, error) => { setLoadFailed(true); }); return () => unsubscribe(); }, [onLoadError]);
if (loadFailed) { return <div className="i18n-error">Some translations failed to load.</div>; }
return <h1>{t('section.title')}</h1>;}<script setup lang="ts">import { useI18n } from '@comvi/vue';import { ref, onMounted, onUnmounted } from 'vue';
const { t, onLoadError } = useI18n();const loadFailed = ref(false);
let unsubscribe: (() => void) | undefined;onMounted(() => { unsubscribe = onLoadError((locale, namespace, error) => { loadFailed.value = true; });});onUnmounted(() => unsubscribe?.());</script>
<template> <div v-if="loadFailed" class="i18n-error"> Some translations failed to load. </div> <h1 v-else>{{ t('section.title') }}</h1></template>'use client';import { useI18n } from '@comvi/next/client';import { useEffect, useState } from 'react';
function TranslatedSection() { const { t, onLoadError } = useI18n(); const [loadFailed, setLoadFailed] = useState(false);
useEffect(() => { const unsubscribe = onLoadError((locale, namespace, error) => { setLoadFailed(true); }); return () => unsubscribe(); }, [onLoadError]);
if (loadFailed) { return <div className="i18n-error">Some translations failed to load.</div>; }
return <h1>{t('section.title')}</h1>;}<script setup lang="ts">const { t, onLoadError } = useI18n();const loadFailed = ref(false);
let unsubscribe: (() => void) | undefined;onMounted(() => { unsubscribe = onLoadError((locale, namespace, error) => { loadFailed.value = true; });});onUnmounted(() => unsubscribe?.());</script>
<template> <div v-if="loadFailed" class="i18n-error"> Some translations failed to load. </div> <h1 v-else>{{ t('section.title') }}</h1></template>import { useI18n } from '@comvi/solid';import { createSignal, onCleanup } from 'solid-js';
function TranslatedSection() { const { t, onLoadError } = useI18n(); const [loadFailed, setLoadFailed] = createSignal(false);
const unsubscribe = onLoadError((locale, namespace, error) => { setLoadFailed(true); }); onCleanup(() => unsubscribe());
return loadFailed() ? <div class="i18n-error">Some translations failed to load.</div> : <h1>{t('section.title')}</h1>;}<script lang="ts">import { useI18n } from '@comvi/svelte';import { onDestroy } from 'svelte';
const { t, onLoadError } = useI18n();let loadFailed = false;
const unsubscribe = onLoadError((locale, namespace, error) => { loadFailed = true;});onDestroy(() => unsubscribe());</script>
{#if loadFailed} <div class="i18n-error">Some translations failed to load.</div>{:else} <h1>{$t('section.title')}</h1>{/if}import { i18n } from './i18n';
const el = document.getElementById('section')!;
i18n.onLoadError((locale, namespace, error) => { el.textContent = 'Some translations failed to load.'; el.className = 'i18n-error';});
el.textContent = i18n.t('section.title');Logging Errors in Production
Section titled “Logging Errors in Production”In production, pipe translation errors to your error tracking service. Here is a Sentry integration example:
import * as Sentry from '@sentry/browser';import { createI18n } from '@comvi/core';import { FetchLoader } from '@comvi/plugin-fetch-loader';
const i18n = createI18n({ locale: 'en', fallbackLocale: 'en', // Catch i18n errors (plugin failures, init errors, load failures, etc.) onError: (error, context) => { // Namespace load errors are expected in some cases (offline, CDN down) // Report at warning level, not error Sentry.captureException(error, { tags: { subsystem: 'i18n', source: context?.source }, extra: context, level: context?.source === 'namespace-load' ? 'warning' : 'error', }); },}) .use(FetchLoader({ cdnUrl: 'https://cdn.comvi.io/your-distribution-id', }));
// Report missing keys as breadcrumbs (not full errors)i18n.onMissingKey((key, locale, namespace) => { Sentry.addBreadcrumb({ category: 'i18n', message: `Missing translation: ${key}`, data: { namespace, locale }, level: 'info', });});Resilient vs. Strict Test Mode
Section titled “Resilient vs. Strict Test Mode”By default, Comvi operates in resilient mode: missing keys return a fallback value, post-processor errors are reported and skipped, and throws inside onError or event listeners are intentionally swallowed by core so a buggy listener can’t crash unrelated code.
The missing-key paths that do propagate exceptions are the onMissingKey option and runtime i18n.onMissingKey() callbacks. They are invoked synchronously from inside t() without an internal try/catch, which makes them the right hook when you want tests to fail on missing keys.
strict: 'dev' enables development diagnostics for missing keys, but it does not throw by itself. If you need hard failures, throw from onMissingKey().
For load and plugin errors, use promise assertions or a buffer-and-assert pattern: collect errors in onError (and/or 'loadError') and assert the buffer is empty in your test teardown.
import { createI18n } from '@comvi/core';
export const i18nErrors: Array<{ error: Error; source?: string }> = [];
export const i18n = createI18n({ locale: 'en', strict: 'dev', // Throwing from onError would be swallowed — collect instead. onError: (error, context) => { i18nErrors.push({ error, source: context?.source }); },});
// Throws here DO propagate out of t(), which fails the test loudly.i18n.onMissingKey((key, locale, namespace) => { throw new Error(`[i18n] Missing key "${key}" in [${locale}/${namespace}]`);});import { defineConfig } from 'vitest/config';
export default defineConfig({ test: { setupFiles: ['./src/i18n.test-setup.ts'], },});import { afterEach, expect } from 'vitest';import { i18nErrors } from './i18n.test-setup';
afterEach(() => { expect(i18nErrors, 'i18n recorded errors during this test').toHaveLength(0); i18nErrors.length = 0;});This pattern gives you strict validation in tests (missing keys throw, load/plugin errors fail the test in afterEach) while keeping production resilient.
Common Errors and Fixes
Section titled “Common Errors and Fixes”Plugin Initialization Failed
Section titled “Plugin Initialization Failed”Error: [i18n] Plugin threw during init
Causes:
- CDN is down or unreachable
- API key is invalid or expired
- Network timeout during first fetch
Fix:
.use(FetchLoader({ cdnUrl: 'https://cdn.comvi.io/your-distribution-id', timeout: 15000, // increase from default 10s}), { required: false }) // make plugin optionalNamespace Load Error
Section titled “Namespace Load Error”Error: [i18n] Failed to load translations for [de/admin]
Causes:
- Namespace doesn’t exist in the project
- CDN 404 or authentication failure
- Network timeout
Fix:
i18n.onLoadError((locale, namespace, error) => { console.warn(`Failed to load ${locale}:${namespace}. Using fallback.`); // Optionally load from fallback map or show error UI});Missing Translation Key
Section titled “Missing Translation Key”Error: t('unknown.key') returns 'unknown.key'
Causes:
- Key doesn’t exist in translations
- Typo in key name
- Namespace mismatch
Fix:
i18n.onMissingKey((key, locale, namespace) => { if (import.meta.env.DEV) { console.warn(`Missing translation: [${locale}/${namespace}] "${key}"`); }});Error Handling Checklist
Section titled “Error Handling Checklist”Use this checklist to make sure your app handles translation errors well:
-
fallbackLocaleis set to your most complete language -
onErroris wired to your error tracking service (catches plugin, init, load, and rendering errors) - Missing-key handlers report gaps without generating alert noise
- Components using
isLoadingshow a skeleton or spinner while translations load - Tests use strict error handling to catch missing keys before they reach production
- Plugins are marked
required: falseif failures should not block app initialization