jsdom/jsdom · error · TypeError

The options object may only set 'characterDataOldValue' to t

Error message

The options object may only set 'characterDataOldValue' to true when 'characterData' is true or not present.

What it means

`characterDataOldValue` requests old character-data values, which is only meaningful when character-data mutations are observed. jsdom throws this TypeError when `characterDataOldValue` is truthy but `characterData` is not explicitly true, per the MutationObserver spec's validation steps.

Source

Thrown at lib/jsdom/living/mutation-observer/MutationObserver-impl.js:47

    if (("attributeOldValue" in options || "attributeFilter" in options) && !("attributes" in options)) {
      options.attributes = true;
    }

    if ("characterDataOldValue" in options & !("characterData" in options)) {
      options.characterData = true;
    }

    if (!options.childList && !options.attributes && !options.characterData) {
      throw new TypeError("The options object must set at least one of 'attributes', 'characterData', or 'childList' " +
        "to true.");
    } else if (options.attributeOldValue && !options.attributes) {
      throw new TypeError("The options object may only set 'attributeOldValue' to true when 'attributes' is true or " +
        "not present.");
    } else if (("attributeFilter" in options) && !options.attributes) {
      throw new TypeError("The options object may only set 'attributeFilter' when 'attributes' is true or not " +
        "present.");
    } else if (options.characterDataOldValue && !options.characterData) {
      throw new TypeError("The options object may only set 'characterDataOldValue' to true when 'characterData' is " +
        "true or not present.");
    }

    const existingRegisteredObserver = target._registeredObserverList.find(registeredObserver => {
      return registeredObserver.observer === this;
    });

    if (existingRegisteredObserver) {
      for (const node of this._nodeList) {
        node._registeredObserverList = node._registeredObserverList.filter(registeredObserver => {
          return registeredObserver.source !== existingRegisteredObserver;
        });
      }

      existingRegisteredObserver.options = options;
    } else {
      target._registeredObserverList.push({
        observer: this,

View on GitHub (pinned to 904cc9cd24)

Solutions

  1. Add `characterData: true` alongside `characterDataOldValue: true`.
  2. Derive it: `if (options.characterDataOldValue) options.characterData = true;` before observe().
  3. Remember `subtree: true` does not replace the characterData flag; set both.
  4. Remove characterDataOldValue if old values are not needed.

Example fix

// before
observer.observe(target, { subtree: true, characterDataOldValue: true }); // TypeError

// after
observer.observe(target, { subtree: true, characterData: true, characterDataOldValue: true });
Defensive patterns

Strategy: validation

Validate before calling

if (options.characterDataOldValue && options.characterData !== true) {
  options.characterData = true;
}

Type guard

function hasValidCharacterDataOptions(o) {
  return !o.characterDataOldValue || o.characterData === true;
}

Try / catch

try {
  observer.observe(target, options);
} catch (e) {
  if (e instanceof TypeError && /characterDataOldValue/.test(e.message)) {
    observer.observe(target, { ...options, characterData: true });
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: `observer.observe(target, { characterDataOldValue: true })` without `characterData: true`; observing with `{ subtree: true, characterDataOldValue: true }` assuming subtree enables character data (it does not); merged options where characterData was lost.

Common situations: Believing subtree: true plus characterDataOldValue is sufficient; config templates listing characterDataOldValue but not characterData; refactors renaming characterData to textChanges.

Related errors


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