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.
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.
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:
- State Mutations: Errors during
this.setState(), such as trying to update state on an unmounted component. - Dynamic Stylesheet Updates: Errors inside
this.updateStylesheet()or when compiling reactive styles using thecsstemplate tag. - Custom Event Dispatching: Errors occurring while creating or dispatching custom events through
this.dispatch(). - Lifecycle Connections: Errors thrown during the component connection or adoption phases, including setups inside
onMount()andonAdoption(). - Lifecycle Disconnections: Errors thrown when the component is being disconnected, including during the mount cleanup callbacks and
onDestroy().
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:
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:
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.
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:
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.