diff --git a/packages/@lwc/engine-core/src/framework/mutation-tracker.ts b/packages/@lwc/engine-core/src/framework/mutation-tracker.ts index 33011bb19c..d179ea9d88 100644 --- a/packages/@lwc/engine-core/src/framework/mutation-tracker.ts +++ b/packages/@lwc/engine-core/src/framework/mutation-tracker.ts @@ -7,6 +7,7 @@ import { isFunction, isNull, isObject, isTrustedSignal } from '@lwc/shared'; import { ReactiveObserver, valueMutated, valueObserved } from '../libs/mutation-tracker'; import { subscribeToSignal } from '../libs/signal-tracker'; +import { safeHasProp } from './utils'; import type { Signal } from '@lwc/signals'; import type { JobFunction, CallbackFunction } from '../libs/mutation-tracker'; import type { VM } from './vm'; @@ -34,15 +35,15 @@ export function componentValueObserved(vm: VM, key: PropertyKey, target: any = { } // The portion of reactivity that's exposed to signals is to subscribe a callback to re-render the VM (templates). - // We check check the following to ensure re-render is subscribed at the correct time. + // We check the following to ensure re-render is subscribed at the correct time. // 1. The template is currently being rendered (there is a template reactive observer) // 2. There was a call to a getter to access the signal (happens during vnode generation) if ( lwcRuntimeFlags.ENABLE_EXPERIMENTAL_SIGNALS && isObject(target) && !isNull(target) && - 'value' in target && - 'subscribe' in target && + safeHasProp(target, 'value') && + safeHasProp(target, 'subscribe') && isFunction(target.subscribe) && isTrustedSignal(target) && // Only subscribe if a template is being rendered by the engine diff --git a/packages/@lwc/engine-core/src/framework/utils.ts b/packages/@lwc/engine-core/src/framework/utils.ts index 2a5163d3a5..2bc586e4fd 100644 --- a/packages/@lwc/engine-core/src/framework/utils.ts +++ b/packages/@lwc/engine-core/src/framework/utils.ts @@ -104,3 +104,16 @@ export function shouldBeFormAssociated(Ctor: LightningElementConstructor) { return ctorFormAssociated && apiFeatureEnabled; } + +// check if a property is in an object, and if the object throws an error merely because we are +// checking if the property exists, return false +export function safeHasProp( + obj: unknown, + prop: K +): obj is Record { + try { + return prop in (obj as any); + } catch (_err) { + return false; + } +} diff --git a/packages/@lwc/integration-karma/test/signal/protocol/index.spec.js b/packages/@lwc/integration-karma/test/signal/protocol/index.spec.js index 4284cb59d9..775661f5a6 100644 --- a/packages/@lwc/integration-karma/test/signal/protocol/index.spec.js +++ b/packages/@lwc/integration-karma/test/signal/protocol/index.spec.js @@ -6,6 +6,7 @@ import Parent from 'x/parent'; import Child from 'x/child'; import DuplicateSignalOnTemplate from 'x/duplicateSignalOnTemplate'; import List from 'x/list'; +import Throws from 'x/throws'; // Note for testing purposes the signal implementation uses LWC module resolution to simplify things. // In production the signal will come from a 3rd party library. @@ -212,6 +213,15 @@ describe('signal protocol', () => { expect(subscribe).not.toHaveBeenCalled(); }); + + it('does not throw an error for objects that throw upon "in" checks', async () => { + const elm = createElement('x-throws', { is: Throws }); + document.body.appendChild(elm); + + await Promise.resolve(); + + expect(elm.shadowRoot.querySelector('h1').textContent).toBe('hello'); + }); }); describe('ENABLE_EXPERIMENTAL_SIGNALS not set', () => { diff --git a/packages/@lwc/integration-karma/test/signal/protocol/x/throws/throws.html b/packages/@lwc/integration-karma/test/signal/protocol/x/throws/throws.html new file mode 100644 index 0000000000..e40c3bd251 --- /dev/null +++ b/packages/@lwc/integration-karma/test/signal/protocol/x/throws/throws.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-karma/test/signal/protocol/x/throws/throws.js b/packages/@lwc/integration-karma/test/signal/protocol/x/throws/throws.js new file mode 100644 index 0000000000..21f0811441 --- /dev/null +++ b/packages/@lwc/integration-karma/test/signal/protocol/x/throws/throws.js @@ -0,0 +1,23 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + foo; + + constructor() { + super(); + + this.foo = new Proxy( + {}, + { + has() { + throw new Error("oh no you don't!"); + }, + } + ); + } + + renderedCallback() { + // access `this.foo` to trigger mutation-tracker.ts + this.bar = this.foo; + } +}