Description
When <cap-widget> is unmounted (or remounted via key= change in React) while an async proof-of-work verification is in flight, the internal promise fires after disconnectedCallback has run — at which point the private field #d has already been set to null. This causes an unhandled promise rejection:
Uncaught (in promise) TypeError: can't access property "state", this[#d] is null
Environment
@cap.js/widget version: 0.1.50
- Browsers tested: Chrome, Firefox
- Framework: React / Next.js
Steps to reproduce
- Place
<cap-widget> on a login form.
- User clicks the widget to start verification (async proof-of-work begins).
- Before verification completes, the component is unmounted — e.g.:
- Successful login triggers navigation away from the page
- A React
key prop changes (forcing remount of the custom element)
- The in-flight async callback resumes, attempts to access
this[#d].state, but #d is null → uncaught TypeError.
Root cause
disconnectedCallback nullifies #d (the internal state object) but does not cancel or abort the async proof-of-work promise still in flight. When the promise resolves, it tries to update this[#d].state on an already-disconnected, cleaned-up element.
Expected behavior
disconnectedCallback should abort or ignore any in-flight async work so that promises resolving after disconnect become no-ops instead of throwing.
Possible fix
Use an AbortController or a boolean guard:
disconnectedCallback() {
this.#disconnected = true;
this.#d = null; // existing cleanup
}
// In async verify continuation:
if (this.#disconnected) return;
this.#d.state = ...;
Workaround (current)
Suppressing the error client-side by filtering unhandledrejection events whose stack traces contain @cap.js/widget:
useEffect(() => {
const handler = (e: PromiseRejectionEvent) => {
const stack: string = (e.reason as Error | undefined)?.stack ?? ```;
if (stack.includes("@cap.js/widget") || stack.includes("cap.js/widget")) {
e.preventDefault();
}
};
window.addEventListener("unhandledrejection", handler);
return () => window.removeEventListener("unhandledrejection", handler);
}, []);
This silences the console noise but the underlying race condition remains.
Happy to open a PR if the fix direction above looks right.
Description
When
<cap-widget>is unmounted (or remounted viakey=change in React) while an async proof-of-work verification is in flight, the internal promise fires afterdisconnectedCallbackhas run — at which point the private field#dhas already been set tonull. This causes an unhandled promise rejection:Environment
@cap.js/widgetversion:0.1.50Steps to reproduce
<cap-widget>on a login form.keyprop changes (forcing remount of the custom element)this[#d].state, but#disnull→ uncaught TypeError.Root cause
disconnectedCallbacknullifies#d(the internal state object) but does not cancel or abort the async proof-of-work promise still in flight. When the promise resolves, it tries to updatethis[#d].stateon an already-disconnected, cleaned-up element.Expected behavior
disconnectedCallbackshould abort or ignore any in-flight async work so that promises resolving after disconnect become no-ops instead of throwing.Possible fix
Use an
AbortControlleror a boolean guard:Workaround (current)
Suppressing the error client-side by filtering
unhandledrejectionevents whose stack traces contain@cap.js/widget:This silences the console noise but the underlying race condition remains.
Happy to open a PR if the fix direction above looks right.