Error Boundaries All
Gracefully handle errors in your components with hierarchical error boundaries, fallback UI, and recovery mechanisms.
How Error Boundaries Work
When an error occurs in a component, WildflowerJS follows this propagation pattern:
Error Propagation
- Error occurs in component (init, action, computed, destroy)
- Component's
onError()handler is called (if defined) - If handler returns
true, error is handled - propagation stops - If handler returns
falseor is missing, error bubbles to parent - Process repeats up the component hierarchy
- Global handlers receive unhandled errors
Error Sources
init()- Component initialization errorsactions- User interaction errorscomputed- Computed property evaluation errorsdestroy()- Cleanup errorsonUpdate()- Update lifecycle errors
Basic Error Handling
Add an onError() method to catch errors in your component:
wildflower.component('user-profile', {
state: {
user: null,
errorMessage: ''
},
init() {
// This might throw if user data is invalid
this.loadUser()
},
loadUser() {
// Simulating an error
throw new Error('Failed to load user data')
},
// Error boundary handler
onError(error, context) {
console.error('Error in user-profile:', error.message)
// Update state to show error UI
this.errorMessage = error.message
// Return true to mark error as handled
// Return false to let it propagate to parent
return true
}
})
<div data-component="user-profile">
<div data-show="errorMessage">
<div class="alert alert-danger">
Error: <span data-bind="errorMessage"></span>
</div>
</div>
<div data-show="!errorMessage">
<!-- Normal content -->
</div>
</div>
Error Context
The onError handler receives the error and context information:
wildflower.component('context-aware', {
state: {},
onError(error, context) {
// error - The Error object with message and stack trace
console.log('Error message:', error.message)
console.log('Stack trace:', error.stack)
// context - Additional information about where the error occurred
console.log('Component:', context?.component) // The component instance
console.log('Lifecycle:', context?.lifecycle) // 'init', 'action', 'computed', 'onUpdate', ...
console.log('Action:', context?.action) // Action name if error was in action
console.log('Method:', context?.methodName) // Method name that threw
console.log('Computed:', context?.computedName) // Computed name when lifecycle is 'computed'
// A handler that claims a specific cause should check these first and
// return false for anything it does not recognise.
return true
}
})
Stores, Plugins, Queries and Pools
A store or a plugin can declare onError too, with the same arguments. It receives errors thrown by the entity's own init() (lifecycle: 'init'), computed properties, tick(), watchers, and methods (context.methodName).
A store or plugin method is usually called by your own code, so what happens to its error depends on whether the entity has an onError. With one, the handler receives the error and the call returns undefined, as a component action does. Without one, the error is thrown to the code that called the method, where a try/catch can handle it.
A pool's onAdd, onRemove and onClear hooks, and an entity method run from a data-action, report to the onError of the component, store or plugin that owns the pool (lifecycle: 'pool-hook' for the hooks). The pool operation still completes, so a throwing onClear no longer leaves a pool half cleared.
A query has no onError of its own. When a function from throws or rejects, the message goes into the query's error field and is also printed to the console, once the retries are used up. A params function that throws is printed as well, and the request is sent without the declared params.
An error that no onError handles is printed to the console in every build, including production.
Error Propagation
Errors bubble up through the component hierarchy until handled:
<!-- Parent catches child errors -->
<div data-component="error-boundary">
<div data-component="risky-child">
<!-- If this throws and doesn't handle, parent catches -->
</div>
</div>
// Parent component acts as error boundary
wildflower.component('error-boundary', {
state: {
hasError: false,
childError: ''
},
onError(error, context) {
// Catches errors from child components
this.hasError = true
this.childError = error.message
return true // Stop propagation
}
})
// Child component with potential errors
wildflower.component('risky-child', {
state: {},
init() {
throw new Error('Something went wrong!')
}
// No onError - error propagates to parent
})
Controlling Propagation
Return false from onError to let the error continue propagating:
wildflower.component('logging-boundary', {
state: {},
onError(error, context) {
// Log the error but let parent handle it
console.warn('Error logged:', error.message)
// Return false to propagate to parent
return false
}
})
Global Error Handlers
Register global handlers to catch errors that bubble up past all component boundaries:
// Register a global error handler
wildflower.onError((error, component) => {
console.error('Unhandled error:', error.message)
console.error('In component:', component?.element?.dataset.component)
// Send to error tracking service
errorTrackingService.capture(error, {
component: component?.element?.dataset.component,
state: component?.state
})
})
// You can register multiple handlers
wildflower.onError((error) => {
// Show user notification
showNotification('An error occurred. Please try again.')
})
// Remove a specific handler
const handler = (error) => console.log(error)
wildflower.onError(handler)
wildflower.offError(handler) // Removes the handler
Fallback UI
Use data-error-fallback to automatically show fallback content when errors occur:
<div data-component="data-viewer" data-error-fallback=".error-fallback">
<!-- Normal content -->
<div class="normal-content">
<span data-bind="data"></span>
</div>
<!-- Fallback shown on error -->
<div class="error-fallback" style="display: none;">
<div class="alert alert-danger">
<h4>Unable to load data</h4>
<p>Please try refreshing the page.</p>
<button data-action="retry">Try Again</button>
</div>
</div>
</div>
Template-based Fallback
Reference a template element for reusable fallback UI:
<!-- Reusable error template -->
<template id="error-template">
<div class="error-state">
<span class="error-icon">!</span>
<h3>Something went wrong</h3>
<p>We're working on fixing this.</p>
</div>
</template>
<!-- Use template as fallback -->
<div data-component="widget" data-error-fallback="#error-template">
<!-- Widget content -->
</div>
Error Recovery
Components can recover from errors and retry operations:
wildflower.component('recoverable-loader', {
state: {
status: 'loading',
data: null,
retryCount: 0
},
async init() {
await this.loadData()
},
async loadData() {
this.status = 'loading'
try {
// Simulated API call that might fail
const response = await fetch('/api/data')
if (!response.ok) throw new Error('Failed to fetch')
this.data = await response.json()
this.status = 'success'
} catch (error) {
throw error // Let onError handle it
}
},
retry() {
this.retryCount++
this.loadData()
},
onError(error) {
this.status = 'error'
// Auto-retry up to 3 times
if (this.retryCount < 3) {
setTimeout(() => this.retry(), 1000 * this.retryCount)
}
return true
}
})
<div data-component="recoverable-loader">
<div data-show="status === 'loading'">
<div class="spinner">Loading...</div>
</div>
<div data-show="status === 'success'">
<div data-bind="data.name"></div>
</div>
<div data-show="status === 'error'">
<p>Failed to load data.</p>
<button data-action="retry">
Retry (<span data-bind="retryCount"></span> attempts)
</button>
</div>
</div>
Sibling Isolation
Errors in one component don't affect its siblings:
<div data-component="dashboard">
<!-- If widget-a throws, widget-b still works -->
<div data-component="widget-a"></div>
<div data-component="widget-b"></div>
<div data-component="widget-c"></div>
</div>
// Widget A has an error
wildflower.component('widget-a', {
init() {
throw new Error('Widget A failed')
},
onError(error) {
console.log('Widget A error handled')
return true
}
})
// Widget B initializes normally despite Widget A's error
wildflower.component('widget-b', {
state: { status: 'ok' },
init() {
console.log('Widget B initialized successfully')
}
})
Best Practices
Do
- Place error boundaries at strategic points (pages, features)
- Log errors for debugging (console, tracking service)
- Provide clear fallback UI with recovery options
- Handle known error cases explicitly
- Use global handlers for error tracking
- Return
truewhen you've handled the error
Don't
- Wrap every component in an error boundary
- Silently swallow errors without logging
- Show technical error messages to users
- Forget to handle async errors properly
- Let errors crash the entire application
- Throw errors in
onErrorhandlers
Error Boundary Patterns
Page-Level Boundary
Wrap entire pages to prevent full app crashes:
wildflower.component('page-boundary', {
state: {
pageError: null
},
onError(error) {
this.pageError = {
message: 'This page encountered an error.',
details: error.message,
timestamp: new Date().toISOString()
}
return true
},
refreshPage() {
window.location.reload()
}
})
Feature-Level Boundary
Isolate features so one broken feature doesn't break others:
wildflower.component('feature-boundary', {
state: {
featureName: '',
isDisabled: false
},
onError(error) {
console.error(`Feature "${this.featureName}" disabled:`, error)
this.isDisabled = true
return true
}
})
Key Features
- Declarative fallback UI - Use
data-error-fallbackattribute, no conditional rendering needed - Propagation control - Return
trueorfalsefromonError()to control bubbling - Global handlers -
wildflower.onError()catches errors application-wide
- Rich error context - Access error details, component info, and lifecycle phase
- Error recovery - Built-in
resetError()method to restore normal operation - Automatic cleanup - Failed component state is properly managed