Skip to content

refactor: attach structured cause to v0 throws#247

Merged
johnleider merged 5 commits into
masterfrom
worktree-error-cause
May 29, 2026
Merged

refactor: attach structured cause to v0 throws#247
johnleider merged 5 commits into
masterfrom
worktree-error-cause

Conversation

@johnleider
Copy link
Copy Markdown
Member

@johnleider johnleider commented May 28, 2026

Summary

Every throw in packages/0/src now uses a typed V0Error class with a stable code discriminant, so consumers (and error trackers) can identify v0 errors without parsing message strings.

Site Code
useContext — missing provider V0_CONTEXT_MISSING (key)
useDate — plugin not installed V0_PLUGIN_MISSING (plugin)
palettes/{ant,leonardo,material} — bad seed hex V0_PALETTE_INVALID_SEED (palette, seed)
palettes/material — unknown variant V0_PALETTE_UNKNOWN_VARIANT (palette, variant)
useLogger/adapters/{pino,consola} — missing instance V0_ADAPTER_INSTANCE_MISSING (adapter)
import { isV0Error } from '@vuetify/v0'

try {
  useContext(myKey)
} catch (err) {
  if (isV0Error(err, 'V0_CONTEXT_MISSING')) {
    // err.code, err.key are typed
    reportToTracker(err.key)
  }
}

Design

Pattern modeled on tRPC's TRPCError and Node's error.code registry:

  • V0Error extends Error with code: V0ErrorCode as a top-level field.
  • Error.cause reserved for wrapping the genuine upstream error so Sentry's LinkedErrors integration renders the chain correctly. cause is forwarded through ErrorOptions and only set when supplied.
  • V0ErrorDetails discriminated union (packages/0/src/types/index.ts) is the source of truth for what codes exist and what payload each carries. Adding a code = extending the union.
  • isV0Error(err, code?) type guard intersects the instance with the matching V0ErrorDetails arm, so post-narrowing err.key / err.plugin / etc. are required (not optional).

Initial design used cause: { code, ... } as the discriminant payload. Review surfaced that this conflicts with the standard semantic of Error.cause — every precedent (Node, tRPC, Stripe SDK, OpenAI/Anthropic SDKs, Sentry's chain walker) uses cause for upstream wrapping, not as a discriminant. Pivoted before merging.

Public surface

Re-exported from @vuetify/v0:

  • V0Error (class)
  • isV0Error (type guard)
  • V0ErrorCode (union of codes)
  • V0ErrorDetails (discriminated union of constructor inputs)

Follow-ups (not in this PR)

  • Lint rule (eslint-plugin-vuetify) — enforce that new throw statements under packages/0/src use V0Error, with documented carve-outs.
  • Testing docs page — a dedicated guide covering mocks, fake timers, plugin testing, error assertions (including isV0Error narrowing), etc.

Verification

  • pnpm typecheck:0 — clean
  • pnpm build:0 — clean
  • pnpm test:run packages/0 — 5862 passed / 18 skipped (added 2 cause-shape assertions to existing throw tests)
  • grep "throw new Error" packages/0/src — 0 results outside test/bench files

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 28, 2026

Open in StackBlitz

commit: 27f8130

Adopt Error.cause on the two user-facing throws where a stable
discriminant beats regex-matching the message:

- useContext: cause = { code: 'V0_CONTEXT_MISSING', key }
- useDate:    cause = { code: 'V0_PLUGIN_MISSING', plugin: 'createDatePlugin' }

Tooling (devtools panels, error overlays, error trackers) can now
identify v0 errors stably without parsing strings.
Introduce V0ErrorCause as the central registry of every `Error.cause`
payload v0 throws, with V0ErrorCode as a convenience alias for the
discriminant field.

Updated throws use `satisfies V0ErrorCause` so the compiler verifies the
discriminant and the payload shape at the call site — typos in 'V0_*'
codes no longer compile, and adding a new code requires extending the
union (a single place to audit).

Consumers gain narrowed payload typing on `err.cause` per code.
…rapping

Drop the `cause: { code, ... }` discriminant pattern. Code lives on a
new V0Error class as a top-level field; Error.cause stays reserved for
wrapping the genuine upstream error so Sentry's LinkedErrors, Datadog,
and Rollbar render the chain correctly (the previous design dropped or
mis-attached the payload because cause-as-data isn't an Error instance).

Modeled on tRPC's TRPCError and Node's error code registry. Discriminated
union (renamed V0ErrorCause -> V0ErrorDetails) is now the constructor
input shape, and exports are surfaced from the package root for typed
consumer catches.

isV0Error(err, code?) collapses the manual instanceof + property checks
into one call and intersects the instance with the matching details arm,
so per-code fields (key, plugin) narrow to required after the guard.

Refs:
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
- https://nodejs.org/api/errors.html#nodejs-error-codes
- https://trpc.io/docs/server/error-handling
- https://docs.sentry.io/platforms/javascript/configuration/integrations/linkederrors/
Comply with the project rule that type guards from #v0/utilities
replace raw === undefined comparisons (style.md, PHILOSOPHY §2.3).

Same-directory sibling import (./helpers) rather than the #v0/utilities
barrel to avoid a circular dependency through utilities/index.ts.
…ters

Sweep the remaining bare `throw new Error(...)` sites in packages/0/src
so isV0Error(err) holds for every error v0 throws.

Adds three V0ErrorDetails arms:
- V0_PALETTE_INVALID_SEED  → ant, leonardo, material seed validation
- V0_PALETTE_UNKNOWN_VARIANT → material variant lookup
- V0_ADAPTER_INSTANCE_MISSING → Pino/Consola logger adapter constructors

Pattern is now uniform: 8/8 throw sites in packages/0/src use V0Error
with a code drawn from the typed registry. The only bare Errors that
remain are in node_modules / third-party APIs we don't own.
@johnleider johnleider force-pushed the worktree-error-cause branch from 8f3d294 to 27f8130 Compare May 28, 2026 18:19
@johnleider johnleider merged commit a8e7fe6 into master May 29, 2026
23 of 25 checks passed
@johnleider johnleider deleted the worktree-error-cause branch May 29, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant