a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals

feat: added error handling severity levels & named errors (#11)

* feat: added error handling severity levels

* feat: structured error handling with specific error types and severity levels

* docs: reorient roadmap

authored by Owais and committed by GitHub 0402c202 34de4ac6

Changed files
+713 -172
docs
lib
+105 -90
ROADMAP.md
··· 20 | | ✓ | [Refactor](#evaluator--binder-hardening) | 21 | v0.5.1 | ✓ | [Error Handling & Diagnostics](#error-handling--diagnostics) (partial) | 22 | v0.6.0 | | [Error Handling & Diagnostics](#error-handling--diagnostics) | 23 - | v0.7.0 | | [Bundle Size Optimization](#bundle-size-optimization) | 24 - | v0.8.0 | | [CSP Compatibility](#csp-compatibility) | 25 - | v0.9.0 | | [DOM Morphing & Streaming](#dom-morphing--streaming) | 26 - | v0.10.0 | | [Scope Inheritance & State Management](#scope-inheritance--state-management) | 27 - | v0.11.0 | | [Background Requests & Reactive Polling](#background-requests--reactive-polling) | 28 - | v0.12.0 | | [Attribute Prefix Support](#attribute-prefix-support) | 29 - | v0.13.0 | | [Persistence & Offline](#persistence--offline) (advanced features) | 30 - | v0.14.0 | | [Inspector & Developer Tools](#inspector--developer-tools) | 31 | v1.0.0 | | [Stable Release](#stable-release) | 32 33 ## Completed ··· 120 - ✓ v0.5.1: Centralized error boundary system for directives and effects 121 - ✓ v0.5.1: Sandbox error wrapping with contextual hints (directive name, expression, element) 122 - ✓ v0.5.1: `$volt.report(error, context)` API for plugin and app-level reporting 123 - - v0.6.0: Enhanced console error messages with directive context 124 - - v0.6.0: Differentiated error levels: warn, error, fatal 125 - - v0.6.0: Documentation: "Understanding VoltX Errors" guide 126 - v0.6.0: Add error handling examples to demo 127 - - v0.14.0: Visual in-DOM error overlays for development mode 128 - - v0.14.0: Runtime health monitor tracking failures 129 - - v0.14.0: Configurable global error policy 130 131 ### Persistence & Offline 132 ··· 135 **Deliverables:** 136 - ✓ Persistent signals (localStorage, sessionStorage, indexedDb) 137 - ✓ Storage plugin (`data-volt-persist`) 138 - - v0.13.0: Storage modifiers on signals (`.local`, `.session`, `.ifmissing`) 139 - - v0.13.0: Sync strategy API (merge, overwrite, patch) for conflict resolution 140 - - v0.13.0: Cache invalidation strategies 141 - - v0.13.0: Offline queue for deferred stream events and HTTP requests 142 - - v0.13.0: Service Worker integration for offline-first apps 143 - - v0.13.0: Background sync for deferred requests 144 - - v0.13.0: Cross-tab synchronization via `BroadcastChannel` 145 146 ### Bundle Size Optimization 147 148 **Goal:** Reduce bundle size to <15KB gzipped while maintaining full feature set. 149 **Outcome:** Lightweight runtime footprint with comprehensive declarative capabilities. 150 **Deliverables:** 151 - - v0.7.0: Audit and tree-shake unused code paths 152 - - v0.7.0: Optimize evaluator and binder implementations 153 - - v0.7.0: Minimize plugin footprint, ensure lazy loading 154 - - v0.7.0: Refactor expression compiler for smaller output 155 - - v0.7.0: Compress constant strings and reduce runtime helpers 156 - - v0.7.0: Optimize signal subscription management 157 - - v0.7.0: Production mode stripping (remove dev-only error messages) 158 - - v0.7.0: Aggressive minification pipeline tuning 159 - - v0.7.0: Target: <15KB gzipped sustained 160 161 ### CSP Compatibility 162 163 **Goal:** Make VoltX.js Content Security Policy compliant without 'unsafe-eval'. 164 **Outcome:** VoltX.js can run in strict CSP environments (no Function constructor). 165 **Deliverables:** 166 - - v0.8.0: Research and design CSP-safe evaluator architecture 167 - - v0.8.0: Evaluate trade-offs: AST interpreter vs limited expression subset 168 - - v0.8.0: Implement CSP-safe expression evaluator (AST-based or restricted syntax) 169 - - v0.8.0: Maintain expression feature parity where possible 170 - - v0.8.0: Fallback mode detection for environments requiring CSP 171 - - v0.8.0: Full test coverage for CSP mode 172 - - v0.8.0: Documentation on CSP limitations and alternatives 173 - - v0.8.0: Bundle split: standard build vs CSP build 174 175 ### DOM Morphing & Streaming 176 177 **Goal:** Add intelligent DOM morphing and Server-Sent Events for real-time updates. 178 **Outcome:** Built-in morphing and SSE streaming for seamless server-driven UI updates. 179 **Deliverables:** 180 - - v0.9.0: Integrate Idiomorph or implement lightweight morphing algorithm 181 - - v0.9.0: `data-volt-morph` attribute for morphing-based swaps 182 - - v0.9.0: Preserve focus, scroll, and input state during morphs 183 - - v0.9.0: Server-Sent Events (SSE) integration 184 - - v0.9.0: `data-volt-stream` attribute for SSE endpoints 185 - - v0.9.0: Automatic reconnection with exponential backoff 186 - - v0.9.0: Signal patching from backend SSE events 187 - - v0.9.0: JSON Patch support for partial updates 188 - - v0.9.0: `data-volt-ignore-morph` for selective exclusion 189 - - v0.9.0: WebSocket as alternative to SSE 190 - - v0.9.0: Unified streaming API across SSE/WebSocket 191 192 ### Scope Inheritance & State Management 193 194 **Goal:** Improve data scoping with optional inheritance for ergonomic nested components. 195 **Outcome:** Flexible scoping patterns for complex component hierarchies. 196 **Deliverables:** 197 - - v0.10.0: Optional scope inheritance via `data-volt-scope="inherit"` 198 - - v0.10.0: Child scopes inherit parent signals with override capability 199 - - v0.10.0: $parent accessor for explicit parent scope access 200 - - v0.10.0: Scoped context providers for dependency injection 201 - - v0.10.0: Enhanced $store with namespacing and modules 202 - - v0.10.0: Cross-scope signal sharing patterns 203 204 ### Background Requests & Reactive Polling 205 206 **Goal:** Enable declarative background data fetching and periodic updates. 207 **Outcome:** VoltX.js elements can fetch or refresh data automatically based on time, visibility, or reactive conditions. 208 **Deliverables:** 209 - - v0.11.0: `data-volt-visible` for fetching when element enters viewport (IntersectionObserver) 210 - - v0.11.0: `data-volt-poll` attribute for periodic background requests 211 - - v0.11.0: Configurable intervals, delays, and signal-based triggers 212 - - v0.11.0: Automatic cancellation when elements unmount 213 - - v0.11.0: Conditional polling tied to reactive signals 214 - - v0.11.0: Background task scheduler with priority management 215 216 ### Attribute Prefix Support 217 218 **Goal:** Support multiple attribute prefix options for developer preference. 219 **Outcome:** VoltX.js supports `voltx-`, `vx-`, and `data-volt-` prefixes. 220 **Deliverables:** 221 - - v0.12.0: Add support for `voltx-*` and `vx-*` attribute prefixes 222 - - v0.12.0: Recommend `vx-*` as primary in documentation 223 - - v0.12.0: Maintain backward compatibility with `data-volt-*` 224 - - v0.12.0: Update demo to use recommended prefix 225 226 ### Inspector & Developer Tools 227 228 **Goal:** Improve developer experience and runtime introspection. 229 **Outcome:** First-class developer ergonomics; VoltX.js is enjoyable to debug and extend. 230 **Deliverables:** 231 - - v0.14.0: Visual in-DOM error overlays for development mode 232 - - v0.14.0: Runtime health monitor tracking failures 233 - - v0.14.0: Configurable global error policy (silent, overlay, throw) 234 - - v0.14.0: Developer overlay for inspecting signals, subscriptions, and effects 235 - - v0.14.0: Time-travel debugging for signal history 236 - - v0.14.0: Signal dependency graph visualization 237 - - v0.14.0: Performance profiling tools 238 - - v0.14.0: Browser console integration (`window.$volt.inspect()`) 239 - - v0.14.0: Dev logging toggle (`Volt.debug = true`) 240 - - v0.14.0: Request/response debugging (HTTP actions, SSE streams) 241 - - v0.14.0: Browser DevTools extension with full integration 242 243 ### Stable Release 244 ··· 254 - Community contribution guide & governance doc 255 256 ## Parking Lot 257 - 258 - ### IIFE Build Support 259 - 260 - Provide an IIFE (Immediately Invoked Function Expression) build target for VoltX.js to support direct `<script>` tag usage without module systems. 261 - 262 - **Deliverables:** 263 - 264 - - IIFE build output (voltx.iife.js) alongside ESM build 265 - - Global `Volt` namespace for browser environments 266 - - CDN-friendly distribution (unpkg, jsdelivr) 267 - - Update build pipeline to generate IIFE bundle 268 - - Document usage: `<script src="voltx.iife.min.js"></script>` 269 - - Ensure plugins work with IIFE build 270 - - Add IIFE examples to documentation 271 272 ### Evaluator & Binder Hardening 273
··· 20 | | ✓ | [Refactor](#evaluator--binder-hardening) | 21 | v0.5.1 | ✓ | [Error Handling & Diagnostics](#error-handling--diagnostics) (partial) | 22 | v0.6.0 | | [Error Handling & Diagnostics](#error-handling--diagnostics) | 23 + | v0.6.0 | | [Bundle Size Optimization](#bundle-size-optimization) | 24 + | v0.6.0 | | [IIFE Build Support](#iife-build-support) | 25 + | v0.7.0 | | [Testing & Benchmarking](#testing--benchmarking) | 26 + | v0.7.0 | | [CSP Compatibility](#csp-compatibility) | 27 + | v0.8.0 | | [DOM Morphing & Streaming](#dom-morphing--streaming) | 28 + | v0.9.0 | | [Scope Inheritance & State Management](#scope-inheritance--state-management) | 29 + | v0.10.0 | | [Background Requests & Reactive Polling](#background-requests--reactive-polling) | 30 + | v0.11.0 | | [Attribute Prefix Support](#attribute-prefix-support) | 31 + | v0.12.0 | | [Persistence & Offline](#persistence--offline) (advanced features) | 32 + | v0.13.0 | | [Inspector & Developer Tools](#inspector--developer-tools) | 33 | v1.0.0 | | [Stable Release](#stable-release) | 34 35 ## Completed ··· 122 - ✓ v0.5.1: Centralized error boundary system for directives and effects 123 - ✓ v0.5.1: Sandbox error wrapping with contextual hints (directive name, expression, element) 124 - ✓ v0.5.1: `$volt.report(error, context)` API for plugin and app-level reporting 125 + - ✓ v0.6.0: Enhanced console error messages with directive context 126 + - ✓ v0.6.0: Differentiated error levels: warn, error, fatal 127 + - ✓ v0.6.0: Documentation: "Understanding VoltX Errors" guide 128 - v0.6.0: Add error handling examples to demo 129 + - v0.13.0: Visual in-DOM error overlays for development mode 130 + - v0.13.0: Runtime health monitor tracking failures 131 + - v0.13.0: Configurable global error policy 132 133 ### Persistence & Offline 134 ··· 137 **Deliverables:** 138 - ✓ Persistent signals (localStorage, sessionStorage, indexedDb) 139 - ✓ Storage plugin (`data-volt-persist`) 140 + - v0.12.0: Storage modifiers on signals (`.local`, `.session`, `.ifmissing`) 141 + - v0.12.0: Sync strategy API (merge, overwrite, patch) for conflict resolution 142 + - v0.12.0: Cache invalidation strategies 143 + - v0.12.0: Offline queue for deferred stream events and HTTP requests 144 + - v0.12.0: Service Worker integration for offline-first apps 145 + - v0.12.0: Background sync for deferred requests 146 + - v0.12.0: Cross-tab synchronization via `BroadcastChannel` 147 148 ### Bundle Size Optimization 149 150 **Goal:** Reduce bundle size to <15KB gzipped while maintaining full feature set. 151 **Outcome:** Lightweight runtime footprint with comprehensive declarative capabilities. 152 **Deliverables:** 153 + - v0.6.0: Audit and tree-shake unused code paths 154 + - v0.6.0: Optimize evaluator and binder implementations 155 + - v0.6.0: Minimize plugin footprint, ensure lazy loading 156 + - v0.6.0: Refactor expression compiler for smaller output 157 + - v0.6.0: Compress constant strings and reduce runtime helpers 158 + - v0.6.0: Optimize signal subscription management 159 + - v0.6.0: Production mode stripping (remove dev-only error messages) 160 + - v0.6.0: Aggressive minification pipeline tuning 161 + - v0.6.0: Target: <15KB gzipped sustained 162 + 163 + ### IIFE Build Support 164 + 165 + **Goal:** Provide an IIFE build target for VoltX.js to support direct `<script>` tag usage without module systems. 166 + **Outcome:** VoltX.js can be used via CDN without build tools or module bundlers. 167 + **Deliverables:** 168 + - v0.6.0: IIFE build output (voltx.iife.js) alongside ESM build 169 + - v0.6.0: Global `Volt` namespace for browser environments 170 + - v0.6.0: CDN-friendly distribution (unpkg, jsdelivr) 171 + - v0.6.0: Update build pipeline to generate IIFE bundle 172 + - v0.6.0: Document usage: `<script src="voltx.iife.min.js"></script>` 173 + - v0.6.0: Ensure plugins work with IIFE build 174 + - v0.6.0: Add IIFE examples to documentation 175 + 176 + ### Testing & Benchmarking 177 + 178 + **Goal:** Establish comprehensive testing infrastructure and performance benchmarking. 179 + **Outcome:** VoltX.js has rigorous end-to-end testing and quantifiable performance metrics against competing frameworks. 180 + **Deliverables:** 181 + - v0.7.0: Playwright-based integration test suite for real browser testing 182 + - v0.7.0: End-to-end tests for all core directives and plugins 183 + - v0.7.0: Cross-browser compatibility tests (Chrome, Firefox, Safari) 184 + - v0.7.0: Memory usage and leak detection benchmarks 185 + - v0.7.0: Bundle size tracking and regression detection 186 + - v0.7.0: Reactivity performance benchmarks (signal updates, computed chains, effect execution) 187 + - v0.7.0: DOM update performance benchmarks 188 + - v0.7.0: CI integration for automated benchmark runs and regression alerts 189 190 ### CSP Compatibility 191 192 **Goal:** Make VoltX.js Content Security Policy compliant without 'unsafe-eval'. 193 **Outcome:** VoltX.js can run in strict CSP environments (no Function constructor). 194 **Deliverables:** 195 + - v0.7.0: Research and design CSP-safe evaluator architecture 196 + - v0.7.0: Evaluate trade-offs: AST interpreter vs limited expression subset 197 + - v0.7.0: Implement CSP-safe expression evaluator (AST-based or restricted syntax) 198 + - v0.7.0: Maintain expression feature parity where possible 199 + - v0.7.0: Fallback mode detection for environments requiring CSP 200 + - v0.7.0: Full test coverage for CSP mode 201 + - v0.7.0: Documentation on CSP limitations and alternatives 202 + - v0.7.0: Bundle split: standard build vs CSP build 203 204 ### DOM Morphing & Streaming 205 206 **Goal:** Add intelligent DOM morphing and Server-Sent Events for real-time updates. 207 **Outcome:** Built-in morphing and SSE streaming for seamless server-driven UI updates. 208 **Deliverables:** 209 + - v0.8.0: Integrate Idiomorph or implement lightweight morphing algorithm 210 + - v0.8.0: `data-volt-morph` attribute for morphing-based swaps 211 + - v0.8.0: Preserve focus, scroll, and input state during morphs 212 + - v0.8.0: Server-Sent Events (SSE) integration 213 + - v0.8.0: `data-volt-stream` attribute for SSE endpoints 214 + - v0.8.0: Automatic reconnection with exponential backoff 215 + - v0.8.0: Signal patching from backend SSE events 216 + - v0.8.0: JSON Patch support for partial updates 217 + - v0.8.0: `data-volt-ignore-morph` for selective exclusion 218 + - v0.8.0: WebSocket as alternative to SSE 219 + - v0.8.0: Unified streaming API across SSE/WebSocket 220 221 ### Scope Inheritance & State Management 222 223 **Goal:** Improve data scoping with optional inheritance for ergonomic nested components. 224 **Outcome:** Flexible scoping patterns for complex component hierarchies. 225 **Deliverables:** 226 + - v0.9.0: Optional scope inheritance via `data-volt-scope="inherit"` 227 + - v0.9.0: Child scopes inherit parent signals with override capability 228 + - v0.9.0: $parent accessor for explicit parent scope access 229 + - v0.9.0: Scoped context providers for dependency injection 230 + - v0.9.0: Enhanced $store with namespacing and modules 231 + - v0.9.0: Cross-scope signal sharing patterns 232 233 ### Background Requests & Reactive Polling 234 235 **Goal:** Enable declarative background data fetching and periodic updates. 236 **Outcome:** VoltX.js elements can fetch or refresh data automatically based on time, visibility, or reactive conditions. 237 **Deliverables:** 238 + - v0.10.0: `data-volt-visible` for fetching when element enters viewport (IntersectionObserver) 239 + - v0.10.0: `data-volt-poll` attribute for periodic background requests 240 + - v0.10.0: Configurable intervals, delays, and signal-based triggers 241 + - v0.10.0: Automatic cancellation when elements unmount 242 + - v0.10.0: Conditional polling tied to reactive signals 243 + - v0.10.0: Background task scheduler with priority management 244 245 ### Attribute Prefix Support 246 247 **Goal:** Support multiple attribute prefix options for developer preference. 248 **Outcome:** VoltX.js supports `voltx-`, `vx-`, and `data-volt-` prefixes. 249 **Deliverables:** 250 + - v0.11.0: Add support for `voltx-*` and `vx-*` attribute prefixes 251 + - v0.11.0: Recommend `vx-*` as primary in documentation 252 + - v0.11.0: Maintain backward compatibility with `data-volt-*` 253 + - v0.11.0: Update demo to use recommended prefix 254 255 ### Inspector & Developer Tools 256 257 **Goal:** Improve developer experience and runtime introspection. 258 **Outcome:** First-class developer ergonomics; VoltX.js is enjoyable to debug and extend. 259 **Deliverables:** 260 + - v0.13.0: Visual in-DOM error overlays for development mode 261 + - v0.13.0: Runtime health monitor tracking failures 262 + - v0.13.0: Configurable global error policy (silent, overlay, throw) 263 + - v0.13.0: Developer overlay for inspecting signals, subscriptions, and effects 264 + - v0.13.0: Time-travel debugging for signal history 265 + - v0.13.0: Signal dependency graph visualization 266 + - v0.13.0: Performance profiling tools 267 + - v0.13.0: Browser console integration (`window.$volt.inspect()`) 268 + - v0.13.0: Dev logging toggle (`Volt.debug = true`) 269 + - v0.13.0: Request/response debugging (HTTP actions, SSE streams) 270 + - v0.13.0: Browser DevTools extension with full integration 271 272 ### Stable Release 273 ··· 283 - Community contribution guide & governance doc 284 285 ## Parking Lot 286 287 ### Evaluator & Binder Hardening 288
+1 -45
docs/cli.md
··· 9 The CLI is available as `create-voltx` on npm: 10 11 ```bash 12 - # Use with pnpm (recommended) 13 pnpm create voltx my-app 14 15 # Use with npm ··· 242 - Event handlers 243 - Counter example 244 245 - Best for: Learning VoltX.js basics, simple interactive pages. 246 - 247 ### With Router 248 249 A multi-page application featuring: ··· 252 - Multiple routes (home, about, contact, 404) 253 - Navigation with `data-volt-navigate` 254 - Route matching with `data-volt-url` 255 - 256 - Best for: Multi-page applications, documentation sites, dashboards. 257 258 ### With Plugins 259 ··· 274 - VoltX.js CSS utilities 275 - Semantic HTML 276 - No JavaScript required 277 - 278 - Best for: Static sites, progressively enhanced pages, CSS-only projects. 279 280 ## Configuration 281 ··· 287 import { defineConfig } from 'vite'; 288 289 export default defineConfig({ 290 - // Custom Vite configuration 291 server: { 292 port: 3000, 293 }, ··· 298 ``` 299 300 See the [Vite documentation](https://vitejs.dev/config/) for all available options. 301 - 302 - ## Troubleshooting 303 - 304 - ### Dev Server Won't Start 305 - 306 - Ensure you're in a VoltX.js project directory with an `index.html` file: 307 - 308 - ```bash 309 - ls index.html 310 - ``` 311 - 312 - If `index.html` is missing, you may not be in a VoltX.js project. 313 - 314 - ### Download Fails 315 - 316 - Check your internet connection and try again. The CLI downloads assets from: 317 - 318 - ```text 319 - https://cdn.jsdelivr.net/npm/voltx.js@{version}/dist/ 320 - ``` 321 - 322 - If jsDelivr is blocked, manually download from the [npm package](https://www.npmjs.com/package/voltx.js). 323 - 324 - ### Build Fails 325 - 326 - Ensure all dependencies are installed: 327 - 328 - ```bash 329 - pnpm install 330 - ``` 331 - 332 - Check for syntax errors in your HTML, CSS, or JavaScript files. 333 - 334 - ## Next Steps 335 - 336 - - Read the [Installation Guide](./installation) for framework setup 337 - - Explore [Usage Patterns](./usage/state) for state management
··· 9 The CLI is available as `create-voltx` on npm: 10 11 ```bash 12 + # Use with pnpm 13 pnpm create voltx my-app 14 15 # Use with npm ··· 242 - Event handlers 243 - Counter example 244 245 ### With Router 246 247 A multi-page application featuring: ··· 250 - Multiple routes (home, about, contact, 404) 251 - Navigation with `data-volt-navigate` 252 - Route matching with `data-volt-url` 253 254 ### With Plugins 255 ··· 270 - VoltX.js CSS utilities 271 - Semantic HTML 272 - No JavaScript required 273 274 ## Configuration 275 ··· 281 import { defineConfig } from 'vite'; 282 283 export default defineConfig({ 284 server: { 285 port: 3000, 286 }, ··· 291 ``` 292 293 See the [Vite documentation](https://vitejs.dev/config/) for all available options.
+201
docs/usage/error-handling.md
···
··· 1 + # Error Handling 2 + 3 + ⚠️ Named error classes and enhanced error handling are unreleased as of writing. This documentation describes features planned for v0.6.0. 4 + 5 + VoltX categorizes all errors by source and severity, wrapping them in named error classes with rich debugging context. This system enables precise error identification, flexible handling strategies, and seamless integration with logging services. 6 + 7 + ## Error Types 8 + 9 + Each error source maps to a specific error class: 10 + 11 + | Identifier | Source | Cause | 12 + | ---------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------- | 13 + | `EvaluatorError` | `evaluator` | Expression evaluation fails in directives (`data-volt-text`, `data-volt-if`, etc.) or computed values | 14 + | `BindingError` | `binding` | Directive setup or execution fails (`data-volt-model` with missing signal, invalid `data-volt-for` syntax, missing parent elements) | 15 + | `EffectError` | `effect` | Effect callbacks, computed signals, or async effects fail during execution or cleanup | 16 + | `HttpError` | `http` | HTTP directives encounter network errors, invalid swap strategies, missing target elements, or parsing failures | 17 + | `PluginError` | `plugin` | Custom plugin handlers fail during initialization or execution | 18 + | `LifecycleError` | `lifecycle` | Lifecycle hooks (`beforeMount`, `afterMount`, `onMount`, etc.) fail during execution | 19 + | `ChargeError` | `charge` | `charge()` encounters invalid `data-volt-state` JSON, malformed configuration, or initialization errors | 20 + | `UserError` | `user` | User code explicitly reports errors via `report()` | 21 + 22 + All error classes extend `VoltError` and set their `name` property accordingly (e.g., `error.name === "HttpError"`). 23 + 24 + ## Severity Levels 25 + 26 + Errors have three severity levels that control console output and execution flow: 27 + 28 + **warn** — Non-critical issues logged via `console.warn`. Execution continues. Use for deprecations, missing optional features, or recoverable configuration issues. 29 + 30 + **error** (default) — Recoverable errors logged via `console.error`. Execution continues but the specific operation fails. Most runtime errors use this level. 31 + 32 + **fatal** — Unrecoverable errors logged via `console.error` and then thrown, halting execution. Reserve for critical initialization failures or corrupted state. 33 + 34 + ## Error Context 35 + 36 + Every VoltX error includes contextual metadata for debugging: 37 + 38 + ```ts 39 + interface VoltError { 40 + name: string; // Error class name 41 + source: ErrorSource; // Error category 42 + level: ErrorLevel; // Severity level 43 + directive?: string; // Failed directive (e.g., "data-volt-text") 44 + expression?: string; // Failed expression 45 + element?: HTMLElement; // DOM element where error occurred 46 + cause: Error; // Original wrapped error 47 + timestamp: number; // Unix timestamp (ms) 48 + context: ErrorContext; // Full context including custom properties 49 + 50 + // HTTP errors only 51 + httpMethod?: string; 52 + httpUrl?: string; 53 + httpStatus?: number; 54 + 55 + // Plugin errors only 56 + pluginName?: string; 57 + 58 + // Lifecycle errors only 59 + hookName?: string; 60 + } 61 + ``` 62 + 63 + ## Handling Errors 64 + 65 + ### Registration 66 + 67 + Register global error handlers with `onError()`. Handlers execute in registration order and receive all errors: 68 + 69 + ```ts 70 + import { onError } from "voltx.js"; 71 + 72 + const cleanup = onError((error) => { 73 + analytics.track("error", error.toJSON()); 74 + 75 + if (error instanceof HttpError) { 76 + showToast(`Request failed: ${error.cause.message}`); 77 + } 78 + }); 79 + 80 + // Cleanup when done 81 + cleanup(); 82 + ``` 83 + 84 + ### Propagation Control 85 + 86 + Call `error.stopPropagation()` to prevent subsequent handlers from running: 87 + 88 + ```ts 89 + onError((error) => { 90 + if (error.source === "http") { 91 + handleHttpError(error); 92 + error.stopPropagation(); 93 + } 94 + }); 95 + 96 + // This handler won't run for HTTP errors 97 + onError((error) => logToConsole(error)); 98 + ``` 99 + 100 + ### Cleanup 101 + 102 + Remove handlers individually via their cleanup function or clear all handlers with `clearErrorHandlers()`. 103 + 104 + ## Console Fallback 105 + 106 + When no handlers are registered, errors log to console based on severity (`console.warn` for warn, `console.error` for error/fatal) with formatted context: 107 + 108 + ```text 109 + [ERROR] [evaluator] Cannot read property 'foo' of undefined | Directive: data-volt-text | Expression: user.foo | Element: <div#app> 110 + Caused by: TypeError: Cannot read property 'foo' of undefined 111 + Element: <div id="app">...</div> 112 + ``` 113 + 114 + Fatal errors throw after logging. 115 + 116 + ## Reporting Errors 117 + 118 + Report errors from user code using `report(error, context)`: 119 + 120 + ```ts 121 + import { report } from "voltx.js"; 122 + 123 + try { 124 + processForm(formElement); 125 + } catch (error) { 126 + report(error as Error, { 127 + source: "user", 128 + level: "warn", 129 + element: formElement as HTMLElement, 130 + formId: formElement.id, 131 + }); 132 + } 133 + ``` 134 + 135 + The `context` object accepts any custom properties beyond the standard fields. 136 + 137 + ## Serialization 138 + 139 + All VoltX errors implement `toJSON()` for serialization to logging services or error tracking systems. 140 + 141 + ### Schema 142 + 143 + ```ts 144 + interface SerializedVoltError { 145 + /** Error class name (e.g., "EvaluatorError", "HttpError") */ 146 + name: string; 147 + /** Full formatted error message with context */ 148 + message: string; 149 + /** Error source category */ 150 + source: 151 + | "evaluator" 152 + | "binding" 153 + | "effect" 154 + | "http" 155 + | "plugin" 156 + | "lifecycle" 157 + | "charge" 158 + | "user"; 159 + 160 + /** Severity level */ 161 + level: "warn" | "error" | "fatal"; 162 + /** Directive name (e.g., "data-volt-text") */ 163 + directive?: string; 164 + /** Expression that failed */ 165 + expression?: string; 166 + /** Unix timestamp in milliseconds */ 167 + timestamp: number; 168 + /** Full error context including custom properties */ 169 + context: { 170 + source: string; 171 + level?: string; 172 + element?: HTMLElement; 173 + directive?: string; 174 + expression?: string; 175 + pluginName?: string; 176 + httpMethod?: string; 177 + httpUrl?: string; 178 + httpStatus?: number; 179 + hookName?: string; 180 + [key: string]: unknown; 181 + }; 182 + /** Original error that was wrapped */ 183 + cause: { name: string; message: string; stack?: string; }; 184 + /** VoltX error stack trace */ 185 + stack?: string; 186 + } 187 + ``` 188 + 189 + ### Usage 190 + 191 + ```ts 192 + import { onError } from "voltx.js"; 193 + 194 + onError((error) => { 195 + fetch("/api/errors", { 196 + method: "POST", 197 + headers: { "Content-Type": "application/json" }, 198 + body: JSON.stringify(error.toJSON()), 199 + }); 200 + }); 201 + ```
+9 -1
lib/src/core/charge.ts
··· 88 if (typeof stateData !== "object" || isNil(stateData) || Array.isArray(stateData)) { 89 report(new Error(`data-volt-state must be a JSON object, got ${typeof stateData}`), { 90 source: "charge", 91 element: el as HTMLElement, 92 directive: "data-volt-state", 93 expression: stateAttr, ··· 100 } catch (error) { 101 report(error as Error, { 102 source: "charge", 103 element: el as HTMLElement, 104 directive: "data-volt-state", 105 expression: stateAttr, ··· 143 if (typeof data !== "object" || isNil(data) || Array.isArray(data)) { 144 report(new Error(`data-volt-store script must contain a JSON object, got: ${typeof data}`), { 145 source: "charge", 146 element: script as HTMLElement, 147 directive: "data-volt-store", 148 }); ··· 151 152 registerStore(data); 153 } catch (error) { 154 - report(error as Error, { source: "charge", element: script as HTMLElement, directive: "data-volt-store" }); 155 } 156 } 157 }
··· 88 if (typeof stateData !== "object" || isNil(stateData) || Array.isArray(stateData)) { 89 report(new Error(`data-volt-state must be a JSON object, got ${typeof stateData}`), { 90 source: "charge", 91 + level: "fatal", 92 element: el as HTMLElement, 93 directive: "data-volt-state", 94 expression: stateAttr, ··· 101 } catch (error) { 102 report(error as Error, { 103 source: "charge", 104 + level: "fatal", 105 element: el as HTMLElement, 106 directive: "data-volt-state", 107 expression: stateAttr, ··· 145 if (typeof data !== "object" || isNil(data) || Array.isArray(data)) { 146 report(new Error(`data-volt-store script must contain a JSON object, got: ${typeof data}`), { 147 source: "charge", 148 + level: "fatal", 149 element: script as HTMLElement, 150 directive: "data-volt-store", 151 }); ··· 154 155 registerStore(data); 156 } catch (error) { 157 + report(error as Error, { 158 + source: "charge", 159 + level: "fatal", 160 + element: script as HTMLElement, 161 + directive: "data-volt-store", 162 + }); 163 } 164 } 165 }
+183 -32
lib/src/core/error.ts
··· 6 * 7 * @module core/error 8 */ 9 - import type { ErrorContext, ErrorHandler, ErrorSource } from "$types/volt"; 10 11 /** 12 - * Enhanced error class with VoltX context 13 * 14 - * Wraps original errors with rich debugging information including 15 - * source, element, directive, and expression details. 16 */ 17 export class VoltError extends Error { 18 /** Error source category */ 19 public readonly source: ErrorSource; 20 /** DOM element where error occurred */ 21 public readonly element?: HTMLElement; 22 /** Directive name */ ··· 38 this.name = "VoltError"; 39 this.cause = cause; 40 this.source = context.source; 41 this.element = context.element; 42 this.directive = context.directive; 43 this.expression = context.expression; ··· 47 // V8-specific feature 48 // See: https://github.com/microsoft/TypeScript/issues/3926 49 if ((Error as any).captureStackTrace) { 50 - (Error as any).captureStackTrace(this, VoltError); 51 } 52 } 53 ··· 67 68 private static buildMessage(cause: Error, context: ErrorContext): string { 69 const parts: string[] = []; 70 71 - parts.push(`[${context.source}] ${cause.message}`); 72 73 if (context.directive) { 74 parts.push(`Directive: ${context.directive}`); ··· 112 name: this.name, 113 message: this.message, 114 source: this.source, 115 directive: this.directive, 116 expression: this.expression, 117 timestamp: this.timestamp, ··· 123 } 124 125 /** 126 * Global error handler registry 127 */ 128 let errorHandlers: ErrorHandler[] = []; ··· 131 * Register an error handler 132 * 133 * Multiple handlers can be registered and will be called in registration order. 134 - * Handlers can call `error.stopPropagation()` to prevent subsequent handlers 135 - * from being called. 136 * 137 * @param handler - Error handler function 138 * @returns Cleanup function to unregister the handler ··· 183 * If no error handlers are registered, errors are logged to console as fallback. 184 * Once handlers are registered, console logging is disabled. 185 * 186 * @param error - Error to report (can be Error, unknown, or string) 187 * @param context - Error context with source and additional details 188 * 189 * @example 190 * ```ts 191 - * // Internal usage (by VoltX) 192 - * try { 193 - * evaluate(expression, scope); 194 - * } catch (err) { 195 - * report(err, { 196 - * source: ErrorSource.Evaluator, 197 - * element: ctx.element, 198 - * directive: 'data-volt-text', 199 - * expression: expression 200 - * }); 201 - * } 202 * 203 - * // External usage (by plugins/apps) 204 - * try { 205 - * myCustomLogic(); 206 - * } catch (err) { 207 - * report(err, { 208 - * source: ErrorSource.User, 209 - * customContext: 'My feature failed' 210 - * }); 211 - * } 212 * ``` 213 */ 214 export function report(error: unknown, context: ErrorContext): void { 215 const errorObj = error instanceof Error ? error : new Error(String(error)); 216 - const voltError = new VoltError(errorObj, context); 217 218 if (errorHandlers.length === 0) { 219 - console.error(voltError.message); 220 - console.error("Caused by:", voltError.cause); 221 if (voltError.element) { 222 - console.error("Element:", voltError.element); 223 } 224 return; 225 } ··· 232 } 233 } catch (handlerError) { 234 console.error("Error in error handler:", handlerError); 235 } 236 } 237 }
··· 6 * 7 * @module core/error 8 */ 9 + import type { ErrorContext, ErrorHandler, ErrorLevel, ErrorSource } from "$types/volt"; 10 11 /** 12 + * Base error class with VoltX context 13 * 14 + * Wraps original errors with rich debugging information including ource, element, directive, and expression details. 15 */ 16 export class VoltError extends Error { 17 /** Error source category */ 18 public readonly source: ErrorSource; 19 + /** Error severity level */ 20 + public readonly level: ErrorLevel; 21 /** DOM element where error occurred */ 22 public readonly element?: HTMLElement; 23 /** Directive name */ ··· 39 this.name = "VoltError"; 40 this.cause = cause; 41 this.source = context.source; 42 + this.level = context.level ?? "error"; 43 this.element = context.element; 44 this.directive = context.directive; 45 this.expression = context.expression; ··· 49 // V8-specific feature 50 // See: https://github.com/microsoft/TypeScript/issues/3926 51 if ((Error as any).captureStackTrace) { 52 + (Error as any).captureStackTrace(this, this.constructor); 53 } 54 } 55 ··· 69 70 private static buildMessage(cause: Error, context: ErrorContext): string { 71 const parts: string[] = []; 72 + const level = context.level ?? "error"; 73 74 + parts.push(`[${level.toUpperCase()}] [${context.source}] ${cause.message}`); 75 76 if (context.directive) { 77 parts.push(`Directive: ${context.directive}`); ··· 115 name: this.name, 116 message: this.message, 117 source: this.source, 118 + level: this.level, 119 directive: this.directive, 120 expression: this.expression, 121 timestamp: this.timestamp, ··· 127 } 128 129 /** 130 + * Error during expression evaluation 131 + * 132 + * Thrown when evaluating expressions in directives like data-volt-text, data-volt-if, or any other binding that uses the expression evaluator. 133 + */ 134 + export class EvaluatorError extends VoltError { 135 + constructor(cause: Error, context: ErrorContext) { 136 + super(cause, { ...context, source: "evaluator" }); 137 + this.name = "EvaluatorError"; 138 + } 139 + } 140 + 141 + /** 142 + * Error during directive binding 143 + * 144 + * Thrown when setting up or executing DOM bindings like data-volt-text, data-volt-class, data-volt-model, etc. 145 + */ 146 + export class BindingError extends VoltError { 147 + constructor(cause: Error, context: ErrorContext) { 148 + super(cause, { ...context, source: "binding" }); 149 + this.name = "BindingError"; 150 + } 151 + } 152 + 153 + /** 154 + * Error during effect execution 155 + * 156 + * Thrown when effects, computed signals, or async effects fail during execution or cleanup. 157 + */ 158 + export class EffectError extends VoltError { 159 + constructor(cause: Error, context: ErrorContext) { 160 + super(cause, { ...context, source: "effect" }); 161 + this.name = "EffectError"; 162 + } 163 + } 164 + 165 + /** 166 + * Error during HTTP operations 167 + * 168 + * Thrown when HTTP directives (data-volt-get, data-volt-post, etc.) encounter network errors, parsing failures, or swap strategy issues. 169 + */ 170 + export class HttpError extends VoltError { 171 + constructor(cause: Error, context: ErrorContext) { 172 + super(cause, { ...context, source: "http" }); 173 + this.name = "HttpError"; 174 + } 175 + } 176 + 177 + /** 178 + * Error in plugin execution 179 + * 180 + * Thrown when custom plugins registered via registerPlugin fail during initialization or execution. 181 + */ 182 + export class PluginError extends VoltError { 183 + constructor(cause: Error, context: ErrorContext) { 184 + super(cause, { ...context, source: "plugin" }); 185 + this.name = "PluginError"; 186 + } 187 + } 188 + 189 + /** 190 + * Error in lifecycle hooks 191 + * 192 + * Thrown when lifecycle hooks (beforeMount, afterMount, onMount, etc.) fail during execution. 193 + */ 194 + export class LifecycleError extends VoltError { 195 + constructor(cause: Error, context: ErrorContext) { 196 + super(cause, { ...context, source: "lifecycle" }); 197 + this.name = "LifecycleError"; 198 + } 199 + } 200 + 201 + /** 202 + * Error during charge/initialization 203 + * 204 + * Thrown when charge() encounters errors during auto-discovery and mounting of [data-volt] elements, or when parsing data-volt-state. 205 + */ 206 + export class ChargeError extends VoltError { 207 + constructor(cause: Error, context: ErrorContext) { 208 + super(cause, { ...context, source: "charge" }); 209 + this.name = "ChargeError"; 210 + } 211 + } 212 + 213 + /** 214 + * User-triggered error 215 + * 216 + * Errors explicitly reported by user code via the report() function 217 + * with source: "user". 218 + */ 219 + export class UserError extends VoltError { 220 + constructor(cause: Error, context: ErrorContext) { 221 + super(cause, { ...context, source: "user" }); 222 + this.name = "UserError"; 223 + } 224 + } 225 + 226 + /** 227 * Global error handler registry 228 */ 229 let errorHandlers: ErrorHandler[] = []; ··· 232 * Register an error handler 233 * 234 * Multiple handlers can be registered and will be called in registration order. 235 + * Handlers can call `error.stopPropagation()` to prevent subsequent handlers from being called. 236 * 237 * @param handler - Error handler function 238 * @returns Cleanup function to unregister the handler ··· 283 * If no error handlers are registered, errors are logged to console as fallback. 284 * Once handlers are registered, console logging is disabled. 285 * 286 + * Error levels determine console output and behavior: 287 + * - warn: Non-critical issues logged with console.warn 288 + * - error: Recoverable errors logged with console.error (default) 289 + * - fatal: Unrecoverable errors logged with console.error and thrown to halt execution 290 + * 291 * @param error - Error to report (can be Error, unknown, or string) 292 * @param context - Error context with source and additional details 293 * 294 * @example 295 * ```ts 296 + * // Warning for non-critical issues 297 + * report(err, { 298 + * source: "binding", 299 + * level: "warn", 300 + * directive: "data-volt-deprecated" 301 + * }); 302 * 303 + * // Error for recoverable issues (default) 304 + * report(err, { 305 + * source: "evaluator", 306 + * level: "error", 307 + * directive: "data-volt-text", 308 + * expression: expression 309 + * }); 310 + * 311 + * // Fatal error that halts execution 312 + * report(err, { 313 + * source: "charge", 314 + * level: "fatal", 315 + * directive: "data-volt-state" 316 + * }); 317 * ``` 318 */ 319 export function report(error: unknown, context: ErrorContext): void { 320 const errorObj = error instanceof Error ? error : new Error(String(error)); 321 + 322 + const voltError = createErrorBySource(errorObj, context); 323 324 if (errorHandlers.length === 0) { 325 + const logFn = voltError.level === "warn" ? console.warn : console.error; 326 + 327 + logFn(voltError.message); 328 + logFn("Caused by:", voltError.cause); 329 if (voltError.element) { 330 + logFn("Element:", voltError.element); 331 + } 332 + 333 + if (voltError.level === "fatal") { 334 + throw voltError; 335 } 336 return; 337 } ··· 344 } 345 } catch (handlerError) { 346 console.error("Error in error handler:", handlerError); 347 + } 348 + } 349 + 350 + if (voltError.level === "fatal") { 351 + throw voltError; 352 + } 353 + } 354 + 355 + /** 356 + * Create the appropriate error type based on the source 357 + */ 358 + function createErrorBySource(cause: Error, context: ErrorContext): VoltError { 359 + switch (context.source) { 360 + case "evaluator": { 361 + return new EvaluatorError(cause, context); 362 + } 363 + case "binding": { 364 + return new BindingError(cause, context); 365 + } 366 + case "effect": { 367 + return new EffectError(cause, context); 368 + } 369 + case "http": { 370 + return new HttpError(cause, context); 371 + } 372 + case "plugin": { 373 + return new PluginError(cause, context); 374 + } 375 + case "lifecycle": { 376 + return new LifecycleError(cause, context); 377 + } 378 + case "charge": { 379 + return new ChargeError(cause, context); 380 + } 381 + case "user": { 382 + return new UserError(cause, context); 383 + } 384 + default: { 385 + return new VoltError(cause, context); 386 } 387 } 388 }
+2
lib/src/core/http.ts
··· 231 default: { 232 report(new Error(`Unknown swap strategy: ${strategy as string}`), { 233 source: "http", 234 element: target as HTMLElement, 235 }); 236 } ··· 510 if (!target) { 511 report(new Error(`Target element not found: ${targetConf}`), { 512 source: "http", 513 element: defaultEl as HTMLElement, 514 directive: "data-volt-target", 515 });
··· 231 default: { 232 report(new Error(`Unknown swap strategy: ${strategy as string}`), { 233 source: "http", 234 + level: "warn", 235 element: target as HTMLElement, 236 }); 237 } ··· 511 if (!target) { 512 report(new Error(`Target element not found: ${targetConf}`), { 513 source: "http", 514 + level: "warn", 515 element: defaultEl as HTMLElement, 516 directive: "data-volt-target", 517 });
+15 -2
lib/src/index.ts
··· 7 export { asyncEffect } from "$core/async-effect"; 8 export { mount } from "$core/binder"; 9 export { charge } from "$core/charge"; 10 - export { clearErrorHandlers, onError, report } from "$core/error"; 11 - export type { VoltError } from "$core/error"; 12 export { parseHttpConfig, request, serializeForm, serializeFormToJSON, swap } from "$core/http"; 13 export { 14 clearAllGlobalHooks, ··· 86 ComputedSignal, 87 ErrorContext, 88 ErrorHandler, 89 ErrorSource, 90 GlobalHookName, 91 GlobalStore,
··· 7 export { asyncEffect } from "$core/async-effect"; 8 export { mount } from "$core/binder"; 9 export { charge } from "$core/charge"; 10 + export { 11 + BindingError, 12 + ChargeError, 13 + clearErrorHandlers, 14 + EffectError, 15 + EvaluatorError, 16 + HttpError, 17 + LifecycleError, 18 + onError, 19 + PluginError, 20 + report, 21 + UserError, 22 + VoltError, 23 + } from "$core/error"; 24 export { parseHttpConfig, request, serializeForm, serializeFormToJSON, swap } from "$core/http"; 25 export { 26 clearAllGlobalHooks, ··· 98 ComputedSignal, 99 ErrorContext, 100 ErrorHandler, 101 + ErrorLevel, 102 ErrorSource, 103 GlobalHookName, 104 GlobalStore,
+11
lib/src/types/volt.d.ts
··· 559 export type ErrorSource = "evaluator" | "binding" | "effect" | "http" | "plugin" | "lifecycle" | "charge" | "user"; 560 561 /** 562 * Context information for error reporting 563 */ 564 export type ErrorContext = { 565 /** Error source category */ 566 source: ErrorSource; 567 /** DOM element where error occurred */ 568 element?: HTMLElement; 569 /** Directive name (e.g., "data-volt-text", "data-volt-on-click") */
··· 559 export type ErrorSource = "evaluator" | "binding" | "effect" | "http" | "plugin" | "lifecycle" | "charge" | "user"; 560 561 /** 562 + * Error severity level 563 + * 564 + * - `warn`: Non-critical issues that don't prevent operation (e.g., deprecated usage, missing optional features) 565 + * - `error`: Recoverable errors that prevent specific operations (e.g., failed evaluations, missing elements) 566 + * - `fatal`: Unrecoverable errors that should halt execution (e.g., critical initialization failures) 567 + */ 568 + export type ErrorLevel = "warn" | "error" | "fatal"; 569 + 570 + /** 571 * Context information for error reporting 572 */ 573 export type ErrorContext = { 574 /** Error source category */ 575 source: ErrorSource; 576 + /** Error severity level (defaults to "error") */ 577 + level?: ErrorLevel; 578 /** DOM element where error occurred */ 579 element?: HTMLElement; 580 /** Directive name (e.g., "data-volt-text", "data-volt-on-click") */
+186 -2
lib/test/core/error.test.ts
··· 1 - import { clearErrorHandlers, getErrorHandlerCount, onError, report, VoltError } from "$core/error"; 2 - import type { ErrorContext, ErrorSource } from "$types/volt"; 3 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 4 5 describe("VoltError", () => { ··· 295 for (const [i, source] of sources.entries()) { 296 const voltError: VoltError = handler.mock.calls[i][0]; 297 expect(voltError.source).toBe(source); 298 } 299 }); 300 });
··· 1 + import { 2 + BindingError, 3 + ChargeError, 4 + clearErrorHandlers, 5 + EffectError, 6 + EvaluatorError, 7 + getErrorHandlerCount, 8 + HttpError, 9 + LifecycleError, 10 + onError, 11 + PluginError, 12 + report, 13 + UserError, 14 + VoltError, 15 + } from "$core/error"; 16 + import type { ErrorContext, ErrorLevel, ErrorSource } from "$types/volt"; 17 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 18 19 describe("VoltError", () => { ··· 309 for (const [i, source] of sources.entries()) { 310 const voltError: VoltError = handler.mock.calls[i][0]; 311 expect(voltError.source).toBe(source); 312 + } 313 + }); 314 + 315 + it("creates correct error types based on source", () => { 316 + const handler = vi.fn(); 317 + onError(handler); 318 + 319 + const testCases: Array<{ source: ErrorSource; errorType: typeof VoltError; name: string }> = [ 320 + { source: "evaluator", errorType: EvaluatorError, name: "EvaluatorError" }, 321 + { source: "binding", errorType: BindingError, name: "BindingError" }, 322 + { source: "effect", errorType: EffectError, name: "EffectError" }, 323 + { source: "http", errorType: HttpError, name: "HttpError" }, 324 + { source: "plugin", errorType: PluginError, name: "PluginError" }, 325 + { source: "lifecycle", errorType: LifecycleError, name: "LifecycleError" }, 326 + { source: "charge", errorType: ChargeError, name: "ChargeError" }, 327 + { source: "user", errorType: UserError, name: "UserError" }, 328 + ]; 329 + 330 + for (const { source } of testCases) { 331 + report(new Error(`Test ${source}`), { source }); 332 + } 333 + 334 + expect(handler).toHaveBeenCalledTimes(testCases.length); 335 + 336 + for (const [i, { errorType, name }] of testCases.entries()) { 337 + const voltError = handler.mock.calls[i][0]; 338 + expect(voltError).toBeInstanceOf(errorType); 339 + expect(voltError).toBeInstanceOf(VoltError); 340 + expect(voltError.name).toBe(name); 341 + } 342 + }); 343 + }); 344 + 345 + describe("Error Levels", () => { 346 + beforeEach(() => { 347 + clearErrorHandlers(); 348 + vi.spyOn(console, "error").mockImplementation(() => {}); 349 + vi.spyOn(console, "warn").mockImplementation(() => {}); 350 + }); 351 + 352 + afterEach(() => { 353 + clearErrorHandlers(); 354 + vi.restoreAllMocks(); 355 + }); 356 + 357 + it("defaults to error level when not specified", () => { 358 + const cause = new Error("Test error"); 359 + const context: ErrorContext = { source: "binding" }; 360 + 361 + const voltError = new VoltError(cause, context); 362 + 363 + expect(voltError.level).toBe("error"); 364 + }); 365 + 366 + it("includes error level in VoltError", () => { 367 + const levels: Array<ErrorLevel> = ["warn", "error", "fatal"]; 368 + 369 + for (const level of levels) { 370 + const cause = new Error(`Test ${level}`); 371 + const context: ErrorContext = { source: "binding", level }; 372 + 373 + const voltError = new VoltError(cause, context); 374 + 375 + expect(voltError.level).toBe(level); 376 + } 377 + }); 378 + 379 + it("includes error level in message", () => { 380 + const levels: Array<ErrorLevel> = ["warn", "error", "fatal"]; 381 + 382 + for (const level of levels) { 383 + const cause = new Error(`Test ${level}`); 384 + const context: ErrorContext = { source: "binding", level }; 385 + 386 + const voltError = new VoltError(cause, context); 387 + 388 + expect(voltError.message).toContain(`[${level.toUpperCase()}]`); 389 + } 390 + }); 391 + 392 + it("includes error level in JSON serialization", () => { 393 + const cause = new Error("Test error"); 394 + const context: ErrorContext = { source: "binding", level: "warn" }; 395 + 396 + const voltError = new VoltError(cause, context); 397 + const json = voltError.toJSON(); 398 + 399 + expect(json.level).toBe("warn"); 400 + }); 401 + 402 + it("uses console.warn for warn level without handlers", () => { 403 + const error = new Error("Warning message"); 404 + const context: ErrorContext = { source: "binding", level: "warn" }; 405 + 406 + report(error, context); 407 + 408 + expect(console.warn).toHaveBeenCalledTimes(2); 409 + expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("[WARN]")); 410 + expect(console.warn).toHaveBeenCalledWith("Caused by:", error); 411 + expect(console.error).not.toHaveBeenCalled(); 412 + }); 413 + 414 + it("uses console.error for error level without handlers", () => { 415 + const error = new Error("Error message"); 416 + const context: ErrorContext = { source: "binding", level: "error" }; 417 + 418 + report(error, context); 419 + 420 + expect(console.error).toHaveBeenCalledTimes(2); 421 + expect(console.error).toHaveBeenCalledWith(expect.stringContaining("[ERROR]")); 422 + expect(console.error).toHaveBeenCalledWith("Caused by:", error); 423 + expect(console.warn).not.toHaveBeenCalled(); 424 + }); 425 + 426 + it("uses console.error for fatal level without handlers", () => { 427 + const error = new Error("Fatal error"); 428 + const context: ErrorContext = { source: "charge", level: "fatal" }; 429 + 430 + expect(() => report(error, context)).toThrow(VoltError); 431 + 432 + expect(console.error).toHaveBeenCalledTimes(2); 433 + expect(console.error).toHaveBeenCalledWith(expect.stringContaining("[FATAL]")); 434 + expect(console.error).toHaveBeenCalledWith("Caused by:", error); 435 + }); 436 + 437 + it("throws error for fatal level after handlers", () => { 438 + const handler = vi.fn(); 439 + onError(handler); 440 + 441 + const error = new Error("Fatal error"); 442 + const context: ErrorContext = { source: "charge", level: "fatal" }; 443 + 444 + expect(() => report(error, context)).toThrow(VoltError); 445 + 446 + expect(handler).toHaveBeenCalledTimes(1); 447 + const voltError = handler.mock.calls[0][0]; 448 + expect(voltError.level).toBe("fatal"); 449 + }); 450 + 451 + it("does not throw for warn level", () => { 452 + const error = new Error("Warning"); 453 + const context: ErrorContext = { source: "http", level: "warn" }; 454 + 455 + expect(() => report(error, context)).not.toThrow(); 456 + }); 457 + 458 + it("does not throw for error level", () => { 459 + const error = new Error("Error"); 460 + const context: ErrorContext = { source: "binding", level: "error" }; 461 + 462 + expect(() => report(error, context)).not.toThrow(); 463 + }); 464 + 465 + it("passes error level to handlers", () => { 466 + const handler = vi.fn(); 467 + onError(handler); 468 + 469 + const levels: Array<ErrorLevel> = ["warn", "error", "fatal"]; 470 + 471 + for (const level of levels) { 472 + try { 473 + report(new Error(`Test ${level}`), { source: "binding", level }); 474 + } catch { /* No-op */ } 475 + } 476 + 477 + expect(handler).toHaveBeenCalledTimes(3); 478 + 479 + for (const [i, level] of levels.entries()) { 480 + const voltError: VoltError = handler.mock.calls[i][0]; 481 + expect(voltError.level).toBe(level); 482 } 483 }); 484 });