Topics

Jump to a topic.

Search questions

Find a question by title, tag, or summary.

How do getters and setters work?

Questions in this chapter (7)
  • Draft
  • easy
  • Askedoften
  • Verified October 2026

Answer

get name() {} and set name(value) {} in an object literal or class body define an accessor property: a property whose attributes are get, set, enumerable and configurable instead of value and writable.

  • Read: [[Get]] finds the accessor and calls the getter with the receiver (the object you read from) as this. No getter means undefined.
  • Write: [[Set]] calls the setter with the new value. No setter means the write fails: silent in sloppy code, a TypeError in strict code ("Cannot set property x of # which has only a getter").
  • No storage: the accessor holds functions, not data. The value lives elsewhere: another property (conventionally _x), a closure, or a private field.

Because the receiver is this, an inherited accessor works on the child: child.balance = 10 runs the setter with this === child, so the data lands on child.

Where it shows up:

  • Validation or normalisation on write.
  • Derived, read-only values (fullName, area).
  • Lazy computation.
  • Built-ins: Map.prototype.size, RegExp.prototype.flags, and Object.prototype.__proto__ are getters.

Details:

  • A literal's accessors are enumerable and configurable. Accessors in a class body live on the prototype and are non-enumerable, so they don't appear in Object.keys or JSON.stringify of an instance.
  • Getter and setter functions get names like get balance and set balance.
  • A getter that reads its own property name (get v() { return this.v; }) recurses until a RangeError.
  • Spread and Object.assign read through the getter and store the plain value. They don't copy the accessor.
  • Object.defineProperty(o, 'k', { get() {…}, set(v) {…} }) creates the same thing programmatically.

Example

const account = {
  _balance: 100,
  get balance() {
    return this._balance;
  },
  set balance(value) {
    if (value < 0) throw new RangeError('Balance cannot be negative');
    this._balance = value;
  },
  get summary() {
    return `balance: ${this._balance}`;
  },
};

account.balance = 150;
console.log(account.balance, account.summary); // → 150 balance: 150

try {
  account.summary = 'x'; // getter only: the write is rejected
} catch (error) {
  console.log(error.message); // → Cannot set property summary of #<Object> which has only a getter
}

const copy = { ...account };
console.log(Object.getOwnPropertyDescriptor(copy, 'balance'));
// → { value: 150, writable: true, enumerable: true, configurable: true }

const child = Object.create(account); // `this` is the receiver, not the holder
child.balance = 10;
console.log(Object.hasOwn(child, '_balance'), account.balance); // → true 150

Pitfalls

  • Naming the backing field the same as the accessor. this.balance = v inside set balance calls itself forever.
  • Expecting a getter-only property to throw in sloppy code. The assignment is silently ignored, so modules and classes (strict) reveal the bug while scripts hide it.
  • Copying with spread and expecting live accessors. The copy has a snapshot, and calls the getter once.
  • Heavy work in a getter that looks like a field read. Every access re-runs it, and side effects surprise readers.

Follow-ups

How do you make a getter lazy and cached?
On first call, replace it with a data property using Object.defineProperty(this, 'key', { value, enumerable: true }), then return the value. Later reads skip the getter. This only works when the object is extensible and the accessor is configurable.
Getter or a method?
A getter reads like data, so it should be cheap, idempotent and side-effect free. Anything that is slow, async or takes arguments should be a method.
Do accessors appear in Object.entries?
Enumerable own accessors do, and Object.entries calls their getters to get the value. Class accessors, being non-enumerable on the prototype, don't.

Exercises

Predict the output

let calls = 0;
const source = {
  get id() {
    calls += 1;
    return calls;
  },
};
const copy = { ...source };
copy.id = 100;
console.log(copy.id, source.id, calls);