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 20 | | ✓ | [Refactor](#evaluator--binder-hardening) | 21 21 | v0.5.1 | ✓ | [Error Handling & Diagnostics](#error-handling--diagnostics) (partial) | 22 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) | 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) | 31 33 | v1.0.0 | | [Stable Release](#stable-release) | 32 34 33 35 ## Completed ··· 120 122 - ✓ v0.5.1: Centralized error boundary system for directives and effects 121 123 - ✓ v0.5.1: Sandbox error wrapping with contextual hints (directive name, expression, element) 122 124 - ✓ 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 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 126 128 - 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 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 130 132 131 133 ### Persistence & Offline 132 134 ··· 135 137 **Deliverables:** 136 138 - ✓ Persistent signals (localStorage, sessionStorage, indexedDb) 137 139 - ✓ 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` 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` 145 147 146 148 ### Bundle Size Optimization 147 149 148 150 **Goal:** Reduce bundle size to <15KB gzipped while maintaining full feature set. 149 151 **Outcome:** Lightweight runtime footprint with comprehensive declarative capabilities. 150 152 **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 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 160 189 161 190 ### CSP Compatibility 162 191 163 192 **Goal:** Make VoltX.js Content Security Policy compliant without 'unsafe-eval'. 164 193 **Outcome:** VoltX.js can run in strict CSP environments (no Function constructor). 165 194 **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 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 174 203 175 204 ### DOM Morphing & Streaming 176 205 177 206 **Goal:** Add intelligent DOM morphing and Server-Sent Events for real-time updates. 178 207 **Outcome:** Built-in morphing and SSE streaming for seamless server-driven UI updates. 179 208 **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 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 191 220 192 221 ### Scope Inheritance & State Management 193 222 194 223 **Goal:** Improve data scoping with optional inheritance for ergonomic nested components. 195 224 **Outcome:** Flexible scoping patterns for complex component hierarchies. 196 225 **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 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 203 232 204 233 ### Background Requests & Reactive Polling 205 234 206 235 **Goal:** Enable declarative background data fetching and periodic updates. 207 236 **Outcome:** VoltX.js elements can fetch or refresh data automatically based on time, visibility, or reactive conditions. 208 237 **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 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 215 244 216 245 ### Attribute Prefix Support 217 246 218 247 **Goal:** Support multiple attribute prefix options for developer preference. 219 248 **Outcome:** VoltX.js supports `voltx-`, `vx-`, and `data-volt-` prefixes. 220 249 **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 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 225 254 226 255 ### Inspector & Developer Tools 227 256 228 257 **Goal:** Improve developer experience and runtime introspection. 229 258 **Outcome:** First-class developer ergonomics; VoltX.js is enjoyable to debug and extend. 230 259 **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 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 242 271 243 272 ### Stable Release 244 273 ··· 254 283 - Community contribution guide & governance doc 255 284 256 285 ## 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 286 272 287 ### Evaluator & Binder Hardening 273 288
+1 -45
docs/cli.md
··· 9 9 The CLI is available as `create-voltx` on npm: 10 10 11 11 ```bash 12 - # Use with pnpm (recommended) 12 + # Use with pnpm 13 13 pnpm create voltx my-app 14 14 15 15 # Use with npm ··· 242 242 - Event handlers 243 243 - Counter example 244 244 245 - Best for: Learning VoltX.js basics, simple interactive pages. 246 - 247 245 ### With Router 248 246 249 247 A multi-page application featuring: ··· 252 250 - Multiple routes (home, about, contact, 404) 253 251 - Navigation with `data-volt-navigate` 254 252 - Route matching with `data-volt-url` 255 - 256 - Best for: Multi-page applications, documentation sites, dashboards. 257 253 258 254 ### With Plugins 259 255 ··· 274 270 - VoltX.js CSS utilities 275 271 - Semantic HTML 276 272 - No JavaScript required 277 - 278 - Best for: Static sites, progressively enhanced pages, CSS-only projects. 279 273 280 274 ## Configuration 281 275 ··· 287 281 import { defineConfig } from 'vite'; 288 282 289 283 export default defineConfig({ 290 - // Custom Vite configuration 291 284 server: { 292 285 port: 3000, 293 286 }, ··· 298 291 ``` 299 292 300 293 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
+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 88 if (typeof stateData !== "object" || isNil(stateData) || Array.isArray(stateData)) { 89 89 report(new Error(`data-volt-state must be a JSON object, got ${typeof stateData}`), { 90 90 source: "charge", 91 + level: "fatal", 91 92 element: el as HTMLElement, 92 93 directive: "data-volt-state", 93 94 expression: stateAttr, ··· 100 101 } catch (error) { 101 102 report(error as Error, { 102 103 source: "charge", 104 + level: "fatal", 103 105 element: el as HTMLElement, 104 106 directive: "data-volt-state", 105 107 expression: stateAttr, ··· 143 145 if (typeof data !== "object" || isNil(data) || Array.isArray(data)) { 144 146 report(new Error(`data-volt-store script must contain a JSON object, got: ${typeof data}`), { 145 147 source: "charge", 148 + level: "fatal", 146 149 element: script as HTMLElement, 147 150 directive: "data-volt-store", 148 151 }); ··· 151 154 152 155 registerStore(data); 153 156 } catch (error) { 154 - report(error as Error, { source: "charge", element: script as HTMLElement, directive: "data-volt-store" }); 157 + report(error as Error, { 158 + source: "charge", 159 + level: "fatal", 160 + element: script as HTMLElement, 161 + directive: "data-volt-store", 162 + }); 155 163 } 156 164 } 157 165 }
+183 -32
lib/src/core/error.ts
··· 6 6 * 7 7 * @module core/error 8 8 */ 9 - import type { ErrorContext, ErrorHandler, ErrorSource } from "$types/volt"; 9 + import type { ErrorContext, ErrorHandler, ErrorLevel, ErrorSource } from "$types/volt"; 10 10 11 11 /** 12 - * Enhanced error class with VoltX context 12 + * Base error class with VoltX context 13 13 * 14 - * Wraps original errors with rich debugging information including 15 - * source, element, directive, and expression details. 14 + * Wraps original errors with rich debugging information including ource, element, directive, and expression details. 16 15 */ 17 16 export class VoltError extends Error { 18 17 /** Error source category */ 19 18 public readonly source: ErrorSource; 19 + /** Error severity level */ 20 + public readonly level: ErrorLevel; 20 21 /** DOM element where error occurred */ 21 22 public readonly element?: HTMLElement; 22 23 /** Directive name */ ··· 38 39 this.name = "VoltError"; 39 40 this.cause = cause; 40 41 this.source = context.source; 42 + this.level = context.level ?? "error"; 41 43 this.element = context.element; 42 44 this.directive = context.directive; 43 45 this.expression = context.expression; ··· 47 49 // V8-specific feature 48 50 // See: https://github.com/microsoft/TypeScript/issues/3926 49 51 if ((Error as any).captureStackTrace) { 50 - (Error as any).captureStackTrace(this, VoltError); 52 + (Error as any).captureStackTrace(this, this.constructor); 51 53 } 52 54 } 53 55 ··· 67 69 68 70 private static buildMessage(cause: Error, context: ErrorContext): string { 69 71 const parts: string[] = []; 72 + const level = context.level ?? "error"; 70 73 71 - parts.push(`[${context.source}] ${cause.message}`); 74 + parts.push(`[${level.toUpperCase()}] [${context.source}] ${cause.message}`); 72 75 73 76 if (context.directive) { 74 77 parts.push(`Directive: ${context.directive}`); ··· 112 115 name: this.name, 113 116 message: this.message, 114 117 source: this.source, 118 + level: this.level, 115 119 directive: this.directive, 116 120 expression: this.expression, 117 121 timestamp: this.timestamp, ··· 123 127 } 124 128 125 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 + /** 126 227 * Global error handler registry 127 228 */ 128 229 let errorHandlers: ErrorHandler[] = []; ··· 131 232 * Register an error handler 132 233 * 133 234 * 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. 235 + * Handlers can call `error.stopPropagation()` to prevent subsequent handlers from being called. 136 236 * 137 237 * @param handler - Error handler function 138 238 * @returns Cleanup function to unregister the handler ··· 183 283 * If no error handlers are registered, errors are logged to console as fallback. 184 284 * Once handlers are registered, console logging is disabled. 185 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 + * 186 291 * @param error - Error to report (can be Error, unknown, or string) 187 292 * @param context - Error context with source and additional details 188 293 * 189 294 * @example 190 295 * ```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 - * } 296 + * // Warning for non-critical issues 297 + * report(err, { 298 + * source: "binding", 299 + * level: "warn", 300 + * directive: "data-volt-deprecated" 301 + * }); 202 302 * 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 - * } 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 + * }); 212 317 * ``` 213 318 */ 214 319 export function report(error: unknown, context: ErrorContext): void { 215 320 const errorObj = error instanceof Error ? error : new Error(String(error)); 216 - const voltError = new VoltError(errorObj, context); 321 + 322 + const voltError = createErrorBySource(errorObj, context); 217 323 218 324 if (errorHandlers.length === 0) { 219 - console.error(voltError.message); 220 - console.error("Caused by:", voltError.cause); 325 + const logFn = voltError.level === "warn" ? console.warn : console.error; 326 + 327 + logFn(voltError.message); 328 + logFn("Caused by:", voltError.cause); 221 329 if (voltError.element) { 222 - console.error("Element:", voltError.element); 330 + logFn("Element:", voltError.element); 331 + } 332 + 333 + if (voltError.level === "fatal") { 334 + throw voltError; 223 335 } 224 336 return; 225 337 } ··· 232 344 } 233 345 } catch (handlerError) { 234 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); 235 386 } 236 387 } 237 388 }
+2
lib/src/core/http.ts
··· 231 231 default: { 232 232 report(new Error(`Unknown swap strategy: ${strategy as string}`), { 233 233 source: "http", 234 + level: "warn", 234 235 element: target as HTMLElement, 235 236 }); 236 237 } ··· 510 511 if (!target) { 511 512 report(new Error(`Target element not found: ${targetConf}`), { 512 513 source: "http", 514 + level: "warn", 513 515 element: defaultEl as HTMLElement, 514 516 directive: "data-volt-target", 515 517 });
+15 -2
lib/src/index.ts
··· 7 7 export { asyncEffect } from "$core/async-effect"; 8 8 export { mount } from "$core/binder"; 9 9 export { charge } from "$core/charge"; 10 - export { clearErrorHandlers, onError, report } from "$core/error"; 11 - export type { VoltError } from "$core/error"; 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"; 12 24 export { parseHttpConfig, request, serializeForm, serializeFormToJSON, swap } from "$core/http"; 13 25 export { 14 26 clearAllGlobalHooks, ··· 86 98 ComputedSignal, 87 99 ErrorContext, 88 100 ErrorHandler, 101 + ErrorLevel, 89 102 ErrorSource, 90 103 GlobalHookName, 91 104 GlobalStore,
+11
lib/src/types/volt.d.ts
··· 559 559 export type ErrorSource = "evaluator" | "binding" | "effect" | "http" | "plugin" | "lifecycle" | "charge" | "user"; 560 560 561 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 + /** 562 571 * Context information for error reporting 563 572 */ 564 573 export type ErrorContext = { 565 574 /** Error source category */ 566 575 source: ErrorSource; 576 + /** Error severity level (defaults to "error") */ 577 + level?: ErrorLevel; 567 578 /** DOM element where error occurred */ 568 579 element?: HTMLElement; 569 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"; 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"; 3 17 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 4 18 5 19 describe("VoltError", () => { ··· 295 309 for (const [i, source] of sources.entries()) { 296 310 const voltError: VoltError = handler.mock.calls[i][0]; 297 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); 298 482 } 299 483 }); 300 484 });