Skip to content

[bug] widget: disconnectedCallback does not cancel in-flight async verify → TypeError: can't access property "state", this[#d] is null #252

@dimitryKamga

Description

@dimitryKamga

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

  1. Place <cap-widget> on a login form.
  2. User clicks the widget to start verification (async proof-of-work begins).
  3. 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)
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions