About nostics
nostics is a TypeScript library for developers who need structured errors with stable codes, fixes, and documentation links. It supports reporters and serialization, and can remove report-only diagnostics from production builds.
Key features
- 01Define typed diagnostic codes
- 02Attach fixes and documentation links
- 03Emit diagnostics through reporters
- 04Serialize diagnostics across processes
- 05Use formatters and Vite build plugins
Use cases
- 01Assign stable codes to build and runtime errors
- 02Send diagnostic records to files or URLs
- 03Collect diagnostics during Vite development
- 04Strip report-only diagnostics from production bundles
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ vercel-labs/ nostics --skill nosticsAfter installing, type/nosticsin Claude Code to confirm it loaded.
Bundled filesreferences/documentation-site.mdreferences/migration.md
Details
From the project READMEnostics
Every error condition becomes a typed Diagnostic (extends Error) with a stable code, docs URL, and actionable fix. Serializable via toJSON().
Diagnostic: name (the code), message/why (interpolated text), fix?, docs?, sources? ('file:line:column'), cause?, toJSON(). Throw it, catch it with instanceof Diagnostic, send toJSON() across process boundaries.
defineDiagnostics
Returns one callable handle per code. Calling a handle builds a fresh Diagnostic, fires every reporter in order, and returns it. throw the return value to raise (reporters still run, so a thrown diagnostic also reports).
import { createConsoleReporter, defineDiagnostics } from 'nostics'
const diagnostics = /*#__PURE__*/ defineDiagnostics({
docsBase: (code) => `https://nuxt.com/e/${code.replace('NUXT_', '').toLowerCase()}`,
reporters: [/*#__PURE__*/ createConsoleReporter()],
codes: {
NUXT_B1001: {
why: 'Could not compile template.',
fix: 'Check the template for syntax errors.',
},
NUXT_B2011: {
why: (p: { src: string }) => `Invalid plugin "${p.src}". src option is required.`,
fix: 'Pass a string path or an object with a `src` to `addPlugin()`.',
},
NUXT_W9001: { why: 'message', docs: false }, // per-code: string overrides docsBase, false opts out
},
})
docsBasestring | (code) => string | undefined: string appends/${code.toLowerCase()}; function returns the full URL (orundefinedto omit).codes: each definition needswhy(string | (params) => string, the only required field, becomesError.message); optionalfix(string | (params) => string),docs(string | false), anddata(object | (params) => object). Each code's resolveddatais typed and included intoJSON().reporters: fired on every call; optional. Theiroptionstypes are intersected; required reporter options become required at the call site. Omit it (or pass[]) for a catalog whose codes are only everthrown: the thrownDiagnosticalready carries the message, so a console reporter would print it once and surface it again from the uncaught error, a visible duplicate. Keep report-only warnings and fatal throws in separate catalogs when one needs a reporter and the other does not.- Param inference: params from
why,fix, anddataare intersected and required at the call site. Ifwhyneeds{ src }andfixneeds{ date }, the call requires{ src, date }.
Call sites
diagnostics.NUXT_B1001() // no params: report only
diagnostics.NUXT_B2011({ src: '/plugins/bad.ts' }) // params first
diagnostics.NUXT_B2011({
src,
cause: originalError,
sources: ['nuxt.config.ts:42:3'],
}) // runtime fields merge in
diagnostics.NUXT_B2011({ src }, { method: 'error' }) // reporter options second
throw diagnostics.NUXT_B2011({ src }) // raise
cause/sources go in the params object; sources matters most for build/config diagnostics where the JS stack points inside the library. Catch with if (err instanceof Diagnostic) then read .name, .message, .fix, .docs, and .data.
Definition data is not present in defineProdDiagnostics(). Its .data is undefined. A dev/prod ternary therefore types .data as the code's data type or undefined.
Reporters
(diagnostic: Diagnostic, options?: Opts) => void. Declaring a required options type makes the second call-site argument required and typed.
| Reporter | Import | Description |
|---|---|---|
createConsoleReporter(options?) | nostics | console[method](formatter(d)). method defaults 'warn' ('log'|'warn'|'error'), formatter defaults formatDiagnostic; both via options, method also overridable per call. |
createFetchReporter(url) | nostics/reporters/fetch | POSTs diagnostic JSON to the URL; failures swallowed. |
createFileReporter(options?) | nostics/reporters/node | Appends NDJSON to a local file (default .nostics.log). |
createDevReporter() | nostics/reporters/dev | Sends toJSON() to the Vite dev server via import.meta.hot.send(). |
import type { DiagnosticReporter } from 'nostics'
const sentryReporter: DiagnosticReporter = (d) =>
sentry.captureMessage(d.message, { tags: { code: d.name } })
const audited: DiagnosticReporter<{ priority: number }> = (d, o) =>
audit.log({ name: d.name, priority: o.priority })
// → audited makes diagnostics.X({...}, { priority: 1 }) required and type-checked.
Formatters
| Formatter | Import | Description |
|---|---|---|
formatDiagnostic | nostics | Plain unicode-decorated string (built-in reporters use it). |
ansiFormatter(colors) | nostics/formatters/ansi | Colorized; accepts a Colors interface (red/yellow/cyan/gray/bold/dim, each (s) => string). |
jsonFormatter | nostics/formatters/json | JSON.stringify(diagnostic) via toJSON(). |
formatDiagnostic output, detail order fixed fix → sources → see, missing fields omitted:
[NUXT_B2011] Invalid plugin `/plugins/bad.ts`. src option is required.
├▶ fix: Pass a string path or an object with a `src` to `addPlugin()`.
├▶ sources: nuxt.config.ts:42:3
╰▶ see: https://nuxt.com/e/b2011
Vite plugins (@nostics/unplugin, dev dependency)
@nostics/unplugin/strip-transform (library authors, build optimization) and @nostics/unplugin/dev-server-collector (app developers, dev-time collection). Both unplugin-based: .vite(), .webpack(), .rollup(), etc.
nosticsStripmarksdefineDiagnostics()/*#__PURE__*/and wraps bare diagnostic expression statements with aNODE_ENVguard so they tree-shake out of production. OptionpackageName?(default'nostics'). Throws/returns/assignments stay (they are behavior). For tracking: relative imports, export the catalog directly, no factory wrappers or deep barrels.- The plugin is optional. The same production output happens with no build transform if the catalog is annotated by hand: put
/*#__PURE__*/beforedefineDiagnostics(and before each reporter factory call inside it (as in every example here), and dev-guard each report-only call site (process.env.NODE_ENV !== 'production' && diagnostics.CODE(p)). Always write the annotations in source; reach for the plugin when report-only call sites are unguarded and you want stripping without touching them.
- The plugin is optional. The same production output happens with no build transform if the catalog is annotated by hand: put
nosticsCollectorlistens forcreateDevReporter()diagnostics over the Vite WebSocket and writes them as NDJSON viacreateFileReporter. Vite-only. OptionslogFile?(default.nostics.log),debug?(default!!process.env.DEBUG).
// vite.config.ts
import { nosticsStrip } from '@nostics/unplugin/strip-transform'
import { nosticsCollector } from '@nostics/unplugin/dev-server-collector'
export default defineConfig({
plugins: [nosticsStrip.vite(), nosticsCollector.vite()],
})
// src/diagnostics.ts — pair the collector with createDevReporter()
import { createConsoleReporter, defineDiagnostics } from 'nostics'
import { createDevReporter } from 'nostics/reporters/dev'
export const diagnostics = /*#__PURE__*/ defineDiagnostics({
reporters: [/*#__PURE__*/ createConsoleReporter(), /*#__PURE__*/ createDevReporter()],
codes: {/* ... */},
})
Production builds
- Report-only diagnostics (bare
diagnostics.X()) should disappear:nosticsStripor hand annotations drop them, then the unused catalog tree-shakes. - Surviving diagnostics (
throw/return/assigned/argument) stay, and each keeps the whole catalog reachable, so everywhy/fixships. Not every library throws in production: if yours only reports, stripping is enough, stop here.
When a library does throw in production, pick defineProdDiagnostics at definition time with a NODE_ENV ternary, so a consumer bundler drops the dev branch (all catalog text):
import { defineDiagnostics, defineProdDiagnostics } from 'nostics'
export const diagnostics =
process.env.NODE_ENV === 'production'
? /*#__PURE__*/ defineProdDiagnostics({ docsBase })
: /*#__PURE__*/ defineDiagnostics({
docsBase,
reporters: [/* ... */],
codes: {/* text */},
})
The accessed code becomes the instance name, docs still derives from docsBase, why points to the docs URL when one exists (empty otherwise), no why/fix text ships. No reporters by default (so a surviving throw doesn't also log and then resurface as the uncaught error); pass reporters to keep prod telemetry. nosticsStrip tracks this ternary like a direct catalog export.
Conventions
- Codes are stable, fully-qualified
PREFIX_XNNNN(Bbuild,Rruntime,Cconfig,Ddeprecation). Never reuse or reassign a published code. - In tests, assert the diagnostic code, not its message or formatted text. Check
.nameon a thrownDiagnosticor[CODE]in reporter output. - Always provide
why; providefixwhenever the solution is known (the most actionable field for humans and agents). Use parameterized templates for runtime values, not string concatenation outside the factory. whyis the diagnosis,fixis the remedy — split them, don't overlap them.whystates only what is wrong;fixstates only what to do. The reporter prints both, so any wording that appears in both is dead weight. When a single source sentence carries both ("A hash must start with '#'. Prefix it with '#'."), cut it in two — diagnosis towhy, remedy tofix— rather than pasting the whole thing intowhyand echoing it infix.fixaccepts a param function too ((p) => ...), so move value-bearing remedies (use "#${p.hash}") into it instead of leaving them inwhy.- Pass
causewhen re-raising; passsourceswhen the JS stack doesn't reflect the user's source. - Split large catalogs by domain (
diagnostics/build.ts,runtime.ts,config.ts, re-exported fromindex.ts), eachdefineDiagnostics()sharingdocsBasewith its own code range.
References
- Migrating an existing library to nostics (replacing
console.warn/console.error/warn()/thrownErrors with diagnostic codes, without changing runtime behavior): followreferences/migration.mdstart to finish. - Building the error-code documentation site (page template, deployment, agent optimization):
references/documentation-site.md.