Error Boundaries All

Gracefully handle errors in your components with hierarchical error boundaries, fallback UI, and recovery mechanisms.

Key Concept: Error boundaries catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of crashing the entire application.

How Error Boundaries Work

When an error occurs in a component, WildflowerJS follows this propagation pattern:

Error Propagation
  1. Error occurs in component (init, action, computed, destroy)
  2. Component's onError() handler is called (if defined)
  3. If handler returns true, error is handled - propagation stops
  4. If handler returns false or is missing, error bubbles to parent
  5. Process repeats up the component hierarchy
  6. Global handlers receive unhandled errors
Error Sources
  • init() - Component initialization errors
  • actions - User interaction errors
  • computed - Computed property evaluation errors
  • destroy() - Cleanup errors
  • onUpdate() - 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
    }
})

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 true when 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 onError handlers

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-fallback attribute, no conditional rendering needed
  • Propagation control - Return true or false from onError() 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