Reference
Diagnostics Reference
Themvora reports recoverable problems through diagnostics rather than throwing. Each one carries a stable code you can assert on, a severity, and a concrete correction.
A diagnostic is plain data. Building one has no side effects, which keeps creation separate from the two environment-specific concerns: how it is formatted, and where it is written. The same diagnostic can therefore be logged, collected, forwarded to the devtools panel, or escalated to a thrown error without any of that logic being duplicated at the call site.
Codes are the contract. Tests assert on codes, not on message text, and the code is what tooling can filter by. Messages are written to be useful but may be reworded between releases — treat them as human output, not as an API.
The diagnostic object
| Field | Type | Description |
|---|---|---|
| code | ThemeDiagnosticCode | Stable identifier. Safe to assert on in tests and to filter by; the message is not. |
| level | ThemeDiagnosticLevel | error, warning, deprecation, or info. Chosen deliberately per diagnostic, and it decides which console method is used. |
| message | string | A concise, actionable description of what is wrong. May be reworded between releases. |
| context | ThemeDiagnosticContext | Structured detail — api, property, path, received, expected, and any extra details payload. |
| hint | string | What to do instead. Present only where there is a concrete correction. |
| docs | string | A link to the entry on this page, derived from the code so call sites never repeat it. |
| cause | unknown | The underlying exception, when the diagnostic reports a failure that wrapped one. |
Development and production
The failure is never hidden in production — the message and the code are always emitted. What changes is how much surrounding detail is attached. In development the structured context, the hint, and the reference link are appended, because that is where they are actionable. In a production build the message is emitted on its own.
Environment detection reads NODE_ENV through a guarded access, so it is safe in a browser bundle and can still be replaced statically by a bundler. When there is no signal at all, Themvora reports the richer form rather than the quieter one: suppressing a correctness failure is worse than an extra line of context.
Repeated occurrences
Diagnostics are deduplicated by default. Reactive code paths call the same API on every render or store update, so a single mistake would otherwise flood the console. The first occurrence is the informative one; identical repeats are suppressed until emission is reset.
That memory is process-wide, which makes it stateful across tests. Call resetDiagnosticEmission() between test cases so each one observes its own emissions.
Code reference
| Code | Level | Reported by | Package |
|---|---|---|---|
| TV_MODE_INVALID | warning | selection.setMode() | @themvora/core |
| TV_THEME_TOKEN_MISSING | returned | validateTheme() | @themvora/core, @themvora/cli |
| TV_A11Y_CONTRAST_VIOLATION | warning | createAccessibilityPlugin() | @themvora/core |
| TV_SCHEDULE_THEME_UNRESOLVED | warning | createScheduledPlugin() | @themvora/core |
| TV_PLUGIN_DESTROY_FAILED | warning | pluginManager.destroy() | @themvora/core |
TV_MODE_INVALIDwarning@themvora/core- What it means
- setMode() received a value that is not light, dark, or system. The selection is left unchanged.
- Common cause
- A mode that came from outside the type system — localStorage, a cookie, an element attribute, a cross-tab message, or a framework prop forwarded from plain JavaScript — was passed in without being validated first.
- Correction
- Validate the incoming value before passing it. Pass one of "light", "dark", or "system", or gate the call behind isThemeMode().
- Affected API
- selection.setMode()
TV_THEME_TOKEN_MISSINGreturned@themvora/core, @themvora/cli- What it means
- A theme definition is missing a required semantic token, or declares no tokens at all. Returned as a ValidationIssue rather than emitted to the console.
- Common cause
- A hand-written theme definition omits one of the required semantic colour tokens, or a partial theme was passed where a complete one was expected.
- Correction
- Add the missing token, or build the theme from a complete base so every required token is present. `themvora validate` reports the same issues in CI.
- Affected API
- validateTheme()
TV_A11Y_CONTRAST_VIOLATIONwarning@themvora/core- What it means
- One or more foreground/background pairs in the active theme fail WCAG AA normal contrast. Reported at error level when the plugin is created with warnOnly: false.
- Common cause
- A colour pair was chosen for appearance rather than measured contrast — most often a muted foreground on a tinted surface, or a brand colour used as text on white.
- Correction
- Adjust the failing pair until it passes, or call validateThemeContrast() directly to inspect every pair that was checked.
- Affected API
- createAccessibilityPlugin()
TV_SCHEDULE_THEME_UNRESOLVEDwarning@themvora/core- What it means
- A schedule was configured, but its light/dark theme pair could not be resolved, so no schedule binding was installed.
- Common cause
- The plugin was given theme names that are not registered on the runtime, or it was left to resolve by mode against a registry that has no matching themes.
- Correction
- Pass lightTheme and darkTheme explicitly, or register themes whose names or modes the schedule can resolve.
- Affected API
- createScheduledPlugin()
TV_PLUGIN_DESTROY_FAILEDwarning@themvora/core- What it means
- A plugin threw from onDestroy() while the manager was tearing down. The remaining plugins were still destroyed and the registry was still cleared.
- Common cause
- onDestroy() assumed the plugin had finished starting. It runs even when initialisation never completed, so state it expects to exist is missing.
- Correction
- Make onDestroy() tolerant of partial initialisation. The original exception is preserved on the diagnostic as cause.
- Affected API
- pluginManager.destroy()
Emitting your own diagnostic
Plugins and integrations are expected to report through the same mechanism rather than calling the console directly, so the severity, the code, the deduplication, and the development/production formatting stay consistent across packages.
createDiagnostic() builds the value, formatDiagnostic() renders it for an environment, and emitDiagnostic() writes it. The emit call accepts a sink, so a diagnostic can be routed into a test double, a log pipeline, or the devtools panel instead of the console.
emitDiagnostic(
createDiagnostic({
code: "TV_MODE_INVALID",
level: "warning",
message: 'setMode() received an unknown mode "purple".',
context: {
api: "setMode",
property: "mode",
received: "purple",
expected: "light | dark | system",
},
hint: "Pass one of the three valid modes, or omit the call.",
}),
);When the offending value comes from outside the type system — storage, a cookie, an element attribute, a cross-tab message — validate it with isThemeMode() first. It is the single shared predicate for the light/dark/system axis, exported so adapters do not each re-implement the comparison.
When a diagnostic is fatal
A diagnostic is recoverable by design. When a caller decides a particular failure cannot be recovered from, it can escalate the same value to a thrown error and keep the code and context intact, so the consumer can branch on the code rather than match on prose.
// A caller that treats this as fatal rather than recoverable: throw ThemeError.fromDiagnostic(diagnostic);
The single-argument form remains valid for failures that do not correspond to a diagnostic.
Related
- Core API reference — the generated reference for the diagnostics model.
- Custom Themes — defining and validating theme definitions.
- Plugins — lifecycle hooks, including the accessibility and scheduled plugins.
- Accessibility — contrast requirements and the checks behind the violation code.