diff --git a/src/client/client-log.ts b/src/client/client-log.ts index f50ed49791a..7114105cf2a 100644 --- a/src/client/client-log.ts +++ b/src/client/client-log.ts @@ -1,5 +1,8 @@ +import type * as d from '../declarations'; import { BUILD } from '@app-data'; +export let consoleError: d.ErrorHandler = (e: any, _?: any) => console.error(e); + export const STENCIL_DEV_MODE = BUILD.isTesting ? ['STENCIL:'] // E2E testing : ['%cstencil', 'color: white;background:#4c47ff;font-weight: bold; font-size:10px; padding:2px 6px; border-radius: 5px']; @@ -10,4 +13,4 @@ export const consoleDevWarn = (...m: any[]) => console.warn(...STENCIL_DEV_MODE, export const consoleDevInfo = (...m: any[]) => console.info(...STENCIL_DEV_MODE, ...m); -export const consoleError = (e: any) => console.error(e); +export const setErrorHandler = (handler: d.ErrorHandler) => consoleError = handler; diff --git a/src/compiler/transformers/update-stencil-core-import.ts b/src/compiler/transformers/update-stencil-core-import.ts index 47a2a80f065..70973429b84 100644 --- a/src/compiler/transformers/update-stencil-core-import.ts +++ b/src/compiler/transformers/update-stencil-core-import.ts @@ -70,4 +70,5 @@ const KEEP_IMPORTS = new Set([ 'forceUpdate', 'getRenderingRef', 'forceModeUpdate', + 'setErrorHandler' ]); diff --git a/src/declarations/stencil-public-runtime.ts b/src/declarations/stencil-public-runtime.ts index fb3fa72a433..c01fc7d23f3 100644 --- a/src/declarations/stencil-public-runtime.ts +++ b/src/declarations/stencil-public-runtime.ts @@ -245,6 +245,8 @@ export declare const Watch: WatchDecorator; export type ResolutionHandler = (elm: HTMLElement) => string | undefined | null; +export type ErrorHandler = (err: any, element?: HTMLElement) => void; + /** * `setMode()` is used for libraries which provide multiple "modes" for styles. */ @@ -312,6 +314,13 @@ export declare function writeTask(task: RafCallback): void; * For further information: https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing */ export declare function readTask(task: RafCallback): void; + +/** + * `setErrorHandler()` can be used to inject a custom global error handler. + * Unhandled exception raised while rendering, during event handling, or lifecycles will trigger the custom event handler. + */ +export declare const setErrorHandler: (handler: ErrorHandler) => void; + /** * This file gets copied to all distributions of stencil component collections. * - no imports diff --git a/src/internal/stencil-core/index.d.ts b/src/internal/stencil-core/index.d.ts index d11bffd41f6..21451c91861 100644 --- a/src/internal/stencil-core/index.d.ts +++ b/src/internal/stencil-core/index.d.ts @@ -44,6 +44,7 @@ export { State, Watch, writeTask, + setErrorHandler, } from '../stencil-public-runtime'; export type { StencilConfig as Config, PrerenderConfig } from '../stencil-public-compiler'; diff --git a/src/internal/stencil-core/index.js b/src/internal/stencil-core/index.js index 15887b391e4..069ba92ab86 100644 --- a/src/internal/stencil-core/index.js +++ b/src/internal/stencil-core/index.js @@ -11,4 +11,5 @@ export { setMode, setAssetPath, writeTask, + setErrorHandler, } from '../client/index.js'; diff --git a/src/runtime/host-listener.ts b/src/runtime/host-listener.ts index 224fa80d30a..5b73060ffb3 100644 --- a/src/runtime/host-listener.ts +++ b/src/runtime/host-listener.ts @@ -1,6 +1,6 @@ import type * as d from '../declarations'; import { BUILD } from '@app-data'; -import { doc, plt, supportsListenerOptions, win } from '@platform'; +import { doc, plt, consoleError, supportsListenerOptions, win } from '@platform'; import { HOST_FLAGS, LISTENER_FLAGS } from '@utils'; export const addHostEventListeners = (elm: d.HostElement, hostRef: d.HostRef, listeners: d.ComponentRuntimeHostListener[], attachParentListeners: boolean) => { @@ -36,15 +36,19 @@ export const addHostEventListeners = (elm: d.HostElement, hostRef: d.HostRef, li }; const hostListenerProxy = (hostRef: d.HostRef, methodName: string) => (ev: Event) => { - if (BUILD.lazyLoad) { - if (hostRef.$flags$ & HOST_FLAGS.isListenReady) { - // instance is ready, let's call it's member method for this event - hostRef.$lazyInstance$[methodName](ev); + try { + if (BUILD.lazyLoad) { + if (hostRef.$flags$ & HOST_FLAGS.isListenReady) { + // instance is ready, let's call it's member method for this event + hostRef.$lazyInstance$[methodName](ev); + } else { + (hostRef.$queuedListeners$ = hostRef.$queuedListeners$ || []).push([methodName, ev]); + } } else { - (hostRef.$queuedListeners$ = hostRef.$queuedListeners$ || []).push([methodName, ev]); + (hostRef.$hostElement$ as any)[methodName](ev); } - } else { - (hostRef.$hostElement$ as any)[methodName](ev); + } catch (e) { + consoleError(e); } }; diff --git a/src/runtime/set-value.ts b/src/runtime/set-value.ts index fea936c9ee2..8eee321a61b 100644 --- a/src/runtime/set-value.ts +++ b/src/runtime/set-value.ts @@ -57,7 +57,7 @@ export const setValue = (ref: d.RuntimeRef, propName: string, newVal: any, cmpMe // fire off each of the watch methods that are watching this property instance[watchMethodName](newVal, oldVal, propName); } catch (e) { - consoleError(e); + consoleError(e, elm); } }); } diff --git a/src/runtime/update-component.ts b/src/runtime/update-component.ts index e19dbe80e81..e135236af09 100644 --- a/src/runtime/update-component.ts +++ b/src/runtime/update-component.ts @@ -117,7 +117,7 @@ const updateComponent = async (hostRef: d.HostRef, instance: any, isInitialLoad: } } } catch (e) { - consoleError(e); + consoleError(e, elm); } } @@ -170,7 +170,7 @@ const callRender = (hostRef: d.HostRef, instance: any) => { hostRef.$flags$ |= HOST_FLAGS.hasRendered; } } catch (e) { - consoleError(e); + consoleError(e, hostRef.$hostElement$); } renderingRef = null; return instance; diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index 70d99ae40a2..6cb8278494e 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -9,7 +9,7 @@ import type * as d from '../../declarations'; import { BUILD } from '@app-data'; import { CMP_FLAGS, HTML_NS, SVG_NS, isDef } from '@utils'; -import { consoleError, doc, plt, supportsShadow } from '@platform'; +import { consoleDevError, doc, plt, supportsShadow } from '@platform'; import { h, isHost, newVNode } from './h'; import { NODE_TYPE, PLATFORM_FLAGS, VNODE_FLAGS } from '../runtime-constants'; import { updateElement } from './update-element'; @@ -52,7 +52,7 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: } if (BUILD.isDev && newVNode.$elm$) { - consoleError( + consoleDevError( `The JSX ${ newVNode.$text$ !== null ? `"${newVNode.$text$}" text` : `"${newVNode.$tag$}" element` } node should not be shared within the same renderer. The renderer caches element lookups in order to improve performance. However, a side effect from this is that the exact same JSX node should not be reused. For more information please see https://stenciljs.com/docs/templating-jsx#avoid-shared-jsx-nodes`,