jestjs/jest · error · Error

<spyOn> : ${methodName} is not declared writable or has no s

Error message

<spyOn> : ${methodName} is not declared writable or has no setter
Usage: spyOn(<object>, <methodName>)

What it means

Thrown by SpyRegistry.spyOn when the property descriptor lacks both writable:true and a setter (spyRegistry.ts:120-126). Jest replaces the method via assignment (obj[methodName] = spy), so a read-only or accessor-less property cannot be overwritten by the standard path.

Source

Thrown at packages/jest-jasmine2/src/jasmine/spyRegistry.ts:121

      if (obj[methodName] && isSpy(obj[methodName])) {
        if (this.respy) {
          return obj[methodName];
        } else {
          throw new Error(
            getErrorMsg(`${methodName} has already been spied upon`),
          );
        }
      }

      let descriptor;
      try {
        descriptor = Object.getOwnPropertyDescriptor(obj, methodName);
      } catch {
        // IE 8 doesn't support `definePropery` on non-DOM nodes
      }

      if (descriptor && !(descriptor.writable || descriptor.set)) {
        throw new Error(
          getErrorMsg(
            `${methodName} is not declared writable or has no setter`,
          ),
        );
      }

      const originalMethod = obj[methodName];
      const spiedMethod = createSpy(methodName, originalMethod);
      let restoreStrategy;

      if (Object.prototype.hasOwnProperty.call(obj, methodName)) {
        restoreStrategy = function () {
          obj[methodName] = originalMethod;
        };
      } else {
        restoreStrategy = function () {
          if (!delete obj[methodName]) {
            obj[methodName] = originalMethod;

View on GitHub (pinned to 8e6d128e4a)

Solutions

  1. Spy on the property accessor instead: spyOn(obj, 'method', 'get') to use the property-spy path.
  2. Define a setter on the object: Object.defineProperty(obj, 'method', { ...descriptor, set(v){}, configurable: true }) before spying.
  3. Spy at the prototype level where the method may be writable: spyOn(MyClass.prototype, 'method').
  4. Avoid freezing objects that need to be spied on, or clone them first.

Example fix

// before
spyOn(config, 'load'); // config is frozen, read-only
// after
spyOn(config, 'load', 'get'); // property spy on the getter
Defensive patterns

Strategy: validation

Validate before calling

const d = Object.getOwnPropertyDescriptor(obj, methodName);
if (d && !d.writable && !d.set) {
  throw new Error(`${String(methodName)} is read-only; spy via the property accessor`);
}
spyOn(obj, methodName);

Type guard

const isWritable = (o: object, k: PropertyKey): boolean => {
  const d = Object.getOwnPropertyDescriptor(o, k);
  return !!d && (!!d.writable || typeof d.set === 'function');
};

Prevention

When it happens

Trigger: spyOn on a frozen object, an object literal with a getter but no setter for that method, a class field declared readonly, or a property defined with writable:false via Object.defineProperty.

Common situations: Spying on methods of a frozen config object; ES2022 public class fields that are defined as non-writable; library that uses getters to lazy-initialise methods; TypeScript readonly fields compiled to non-writable descriptors.

Related errors


AI-assisted analysis of jestjs/jest@8e6d128e4a (2026-08-10). Data as JSON: /api/errors/3be1158c144a8be2. Report an issue: GitHub.