Error Handling

Handling runtime errors gracefully is crucial for building robust web applications. @beforesemicolon/web-component features a centralized onError() hook that allows you to intercept and handle errors occurring within the component boundary.

The onError Hook

By default, the onError hook logs the error to the console using console.error.

typescript
1onError(error: Error | unknown): void {2    console.error(error);3}

You can override this method to customize how your component handles errors, such as showing a toast notification, logging to an external service, or dispatching an error event.

javascript
1import { WebComponent, html } from '@beforesemicolon/web-component'2 3class ErrorProneComponent extends WebComponent {4    onMount() {5        throw new Error('Failed to start component')6    }7 8    onError(error) {9        telemetry.logError(error)10        this.dispatch('componenterror', {11            message: error instanceof Error ? error.message : String(error),12        })13    }14 15    render() {16        return html`<p>Starting...</p>`17    }18}

What Triggers onError?

The component automatically wraps internal processes in try/catch blocks. If any of the following operations fail, the error is caught and passed to the onError hook:


Centralized Error Tracking Pattern

In larger applications, repeating error handling logic in every component is inefficient. The recommended pattern is to build a base Component class that extends WebComponent to centralize logging and error telemetry across all components.

Here is an example of a base class setup:

typescript
1// src/components/base-component.ts2import { WebComponent, ObjectInterface } from '@beforesemicolon/web-component'3 4export abstract class BaseComponent<5    P extends ObjectInterface<P> = Record<string, unknown>,6    S extends ObjectInterface<S> = Record<string, unknown>,7> extends WebComponent<P, S> {8    onError(error: Error | unknown) {9        const errorDetails = {10            tagName: this.tagName.toLowerCase(),11            message: error instanceof Error ? error.message : String(error),12            stack: error instanceof Error ? error.stack : undefined,13            timestamp: new Date().toISOString(),14        }15 16        // 1. Log to console17        console.error(`[BaseComponent Error] <${errorDetails.tagName}>:`, error)18 19        // 2. Report to third-party error monitoring tool (e.g. Sentry, LogRocket)20        if (window.errorTracker) {21            window.errorTracker.captureException(error, { extra: errorDetails })22        }23    }24}

Now, instead of extending WebComponent directly, your feature components extend BaseComponent:

typescript
1// src/components/user-profile.ts2import { BaseComponent } from './base-component.ts'3import { html } from '@beforesemicolon/web-component'4 5class UserProfile extends BaseComponent {6    render() {7        return html`<div>User Profile</div>`8    }9}10 11customElements.define('user-profile', UserProfile)

Reporting Your Own Component Errors

onError() is not only for errors WebComponent catches internally. If your component has its own async work, event handlers, or imperative code, catch those errors locally and call this.onError(error).

This is especially useful when all components extend a shared base component. The base class becomes the single reporting boundary, while individual components decide which local failures should be reported.

typescript
1import { html } from '@beforesemicolon/web-component'2import { BaseComponent } from './base-component.ts'3 4class UserProfile extends BaseComponent {5    static observedAttributes = ['user-id']6 7    userId = ''8 9    async loadUser() {10        try {11            const response = await fetch(`/api/users/${this.props.userId()}`)12 13            if (!response.ok) {14                throw new Error(`Failed to load user ${this.props.userId()}`)15            }16 17            const user = await response.json()18            this.setState({ user })19        } catch (error) {20            this.onError(error)21        }22    }23 24    onMount() {25        this.loadUser()26    }27 28    render() {29        return html`<section>User profile</section>`30    }31}

You can use the same approach inside event handlers:

typescript
1handleSave = async () => {2    try {3        await saveSettings(this.state.settings())4        this.dispatch('saved')5    } catch (error) {6        this.onError(error)7    }8}

This keeps the component's local control flow explicit while still routing every report through the same base onError() implementation.

edit this doc