+105
-90
ROADMAP.md
+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
+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
+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
+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
+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
+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
+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
+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
+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
});