jestjs/jest · error · Error

${methodName} is not declared writable or has no setter

Error message

${methodName} is not declared writable or has no setter

What it means

Thrown by `SpyRegistry.spyOn` in spyRegistry.ts:121 when the property descriptor exists but is neither `writable` nor has a `set` accessor (`!(descriptor.writable || descriptor.set)`). jasmine2 replaces the value in place, so a frozen/read-only property without a setter cannot be overwritten.

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 f49721c78e)

Solutions

  1. If it is a getter/setter, use `spyOnProperty(obj, 'prop', 'get')` instead of `spyOn`.
  2. If the object is frozen, spy on the prototype or unfreeze/make the property writable before spying.
  3. Refactor the production code so the dependency is injectable rather than a hardcoded readonly property.

Example fix

// before
spyOn(config, 'timeout') // timeout is a getter
// after
spyOnProperty(config, 'timeout', 'get').and.returnValue(5000)
Defensive patterns

Strategy: validation

Validate before calling

const d = Object.getOwnPropertyDescriptor(obj, methodName);
if (d && !d.writable && !d.set) { /* use spyOnProperty with access type, or refactor */ }

Type guard

const isSpyable = (o: object, m: string): boolean => {
  const d = Object.getOwnPropertyDescriptor(o, m);
  return !d || Boolean(d.writable) || Boolean(d.set);
};

Prevention

When it happens

Trigger: Spying on a method declared `readonly` in a class, a property on a `Object.freeze`d object, a getter-only property without specifying the access type, or an inherited non-writable property.

Common situations: Trying to spy on a getter/setter with the two-arg `spyOn` instead of `spyOnProperty`; class with `readonly` TS fields compiled to non-writable descriptors; frozen config objects.

Related errors


AI-assisted analysis of jestjs/jest@f49721c78e (2026-08-03). Data as JSON: /data/errors/130caace7d3109a1.json. Report an issue: GitHub.