jsdom/jsdom · error · TypeError

Constructor argument is not a constructor.

Error message

Constructor argument is not a constructor.

What it means

customElements.define(name, constructor) requires its second argument to be a callable constructible function/class whose [[Construct]] is usable as a custom element. jsdom checks `isConstructor(ctor)` before anything else and throws this TypeError when the argument is not a real constructor. Since jsdom wraps window objects, this can also happen when a class from a different realm/window is passed.

Source

Thrown at lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js:71

}

// https://html.spec.whatwg.org/#customelementregistry
class CustomElementRegistryImpl {
  constructor(globalObject) {
    this._customElementDefinitions = [];
    this._elementDefinitionIsRunning = false;
    this._whenDefinedPromiseMap = Object.create(null);

    this._globalObject = globalObject;
  }

  // https://html.spec.whatwg.org/#dom-customelementregistry-define
  define(name, constructor, options) {
    const { _globalObject } = this;
    const ctor = constructor.objectReference;

    if (!isConstructor(ctor)) {
      throw new TypeError("Constructor argument is not a constructor.");
    }

    if (!isValidCustomElementName(name)) {
      throw DOMException.create(_globalObject, ["Name argument is not a valid custom element name.", "SyntaxError"]);
    }

    const nameAlreadyRegistered = this._customElementDefinitions.some(entry => entry.name === name);
    if (nameAlreadyRegistered) {
      throw DOMException.create(_globalObject, [
        "This name has already been registered in the registry.",
        "NotSupportedError"
      ]);
    }

    const ctorAlreadyRegistered = this._customElementDefinitions.some(entry => entry.objectReference === ctor);
    if (ctorAlreadyRegistered) {
      throw DOMException.create(_globalObject, [
        "This constructor has already been registered in the registry.",

View on GitHub (pinned to 904cc9cd24)

Solutions

  1. Ensure the second argument is a real class: `customElements.define('x-foo', class extends HTMLElement {...})`.
  2. If the class lives outside the jsdom window, run the define call inside the window's script context (e.g. `window.eval` or `<script>` inside the JSDOM instance) so the class belongs to the same realm and extends the window's HTMLElement.
  3. Verify with `typeof C === 'function' && C.prototype && C.prototype.constructor === C` before calling define.
  4. Check argument order — a valid constructor in the wrong slot (e.g. define(class, 'x-foo')) will also trigger this.

Example fix

// before
customElements.define('x-foo', { connectedCallback() {} });
// after
customElements.define('x-foo', class extends window.HTMLElement { connectedCallback() {} });
Defensive patterns

Strategy: type-guard

Validate before calling

function isRealConstructor(v) {
  return typeof v === 'function' && typeof v.prototype === 'object' && v.prototype !== null && /^class\s|[A-Za-z]+\s*\{/.test(Function.prototype.toString.call(v).slice(0, 200)) && (() => { try { Reflect.construct(function(){}, [], v); return true; } catch { return false; } })();
}
if (!isRealConstructor(C)) throw new TypeError('define requires a class constructor');

Type guard

const isClass = (v) => typeof v === 'function' &&
  (() => { try { new Proxy(v, { construct() { return {}; } }); return true; } catch { return false; } })() &&
  !Object.prototype.hasOwnProperty.call(v, 'call');

Try / catch

try {
  window.customElements.define(name, C);
} catch (e) {
  if (e instanceof TypeError && e.message.includes('not a constructor')) {
    // fall back to defining inside the window realm via window.eval
  } else { throw e; }
}

Prevention

When it happens

Trigger: Calling customElements.define('x-foo', notAClass) — e.g. passing an object literal, an arrow function (not constructor-capable), a bound function, undefined/null, or a class defined in a different jsdom window realm via `new JSDOM().window.customElements.define(...)` with a class from the outer Node realm.

Common situations: Typo passing options object as the second argument; spreading define(name, ...args) incorrectly; passing a decorator-produced value; cross-realm class (class defined in Node vs inside the jsdom window) which fails jsdom's constructor check.

Related errors


AI-assisted analysis of jsdom/jsdom@904cc9cd24 (2026-09-01). Data as JSON: /api/errors/6e6a7260d81bb4f7. Report an issue: GitHub.