Error Boundaries CORE+
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('Action:', context?.action) // Action name if error was in action
console.log('Method:', context?.methodName) // Method name that threw
return true
}
})
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