JavaScriptGetters & Setters

Getters and Setters

A getter is a function that runs when you read a property; a setter runs when you write to it. From the outside the property looks like a plain field, but you can compute it, validate it, or trigger a side effect on each access. They are the cleanest way to migrate a value into a calculation without changing every caller.

The class syntax

JS
class Circle {
  constructor(radius) {
    this.radius = radius;
  }

  get diameter() {
    return this.radius * 2;
  }

  set diameter(value) {
    this.radius = value / 2;
  }
}

const c = new Circle(5);
console.log(c.diameter);   // 10 — getter
c.diameter = 30;            // setter
console.log(c.radius);     // 15

No parentheses on read or write — the property is the computed value to callers. That is the point: a refactor from public field to derived value does not break any caller.

Validation in setters

Setters are the natural place to enforce invariants. Throw, clamp, or log — whatever the design needs.

JS
class Temperature {
  #celsius;

  constructor(celsius) {
    this.celsius = celsius;     // goes through the setter
  }

  get celsius() { return this.#celsius; }

  set celsius(value) {
    if (typeof value !== "number" || Number.isNaN(value)) {
      throw new TypeError("celsius must be a number");
    }
    if (value < -273.15) {
      throw new RangeError("below absolute zero");
    }
    this.#celsius = value;
  }
}

const t = new Temperature(20);
console.log(t.celsius);   // 20

try {
  t.celsius = -300;
} catch (e) {
  console.log("rejected:", e.message);
}

The private field #celsius (covered in Private Fields) stores the real number. The getter and setter become the only way in or out.

Getters with no setter

A getter without a matching setter creates a read-only property. Assignments are silently ignored in sloppy mode, and throw in strict mode (which classes always use).

JS
class Rectangle {
  constructor(w, h) { this.width = w; this.height = h; }
  get area() { return this.width * this.height; }
}

const r = new Rectangle(3, 4);
console.log(r.area);    // 12

try {
  r.area = 99;
} catch (e) {
  console.log("blocked:", e.message);
}
Outside of classes — in object literals

Getters and setters work in plain object literals too. Same get and set keywords, same semantics.

JS
const profile = {
  firstName: "Ada",
  lastName: "Lovelace",

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  set fullName(value) {
    [this.firstName, this.lastName] = value.split(" ");
  },
};

console.log(profile.fullName);   // 'Ada Lovelace'
profile.fullName = "Grace Hopper";
console.log(profile.firstName);  // 'Grace'
Defining accessors with defineProperty

If you cannot use the literal syntax — for example, when adding an accessor to an existing object — Object.defineProperty accepts a get/set descriptor.

JS
const obj = { _count: 0 };

Object.defineProperty(obj, "count", {
  get() { return this._count; },
  set(value) {
    if (value < 0) throw new RangeError("negative");
    this._count = value;
  },
  enumerable: true,
  configurable: true,
});

obj.count = 5;
console.log(obj.count);   // 5
The computed-property gotcha

In an object literal, get x() { ... } is an accessor declaration. But if you wrote { x: function () { return 1 } } and accessed obj.x(), that is a regular method — the parser distinguishes by the get/set keyword. With computed property names, you cannot mix the two:

JS
const key = "fullName";

// SyntaxError — you cannot do "get [key]()" with the keyword in computed names
// const o = { get [key]() { return "..." } };

// Workaround — defineProperty supports computed accessors fine
const o = {};
Object.defineProperty(o, key, {
  get() { return "computed"; },
  configurable: true,
});
Beware infinite recursion
A setter that writes to *itself* loops forever. `set name(value) { this.name = value; }` calls the setter again. Use a backing field — a private `#name` or a convention like `_name` — to store the actual value.
When to reach for them
  • A property that is derived from others: fullName, area, total.

  • A property that needs validation on assignment.

  • Lazy initialisation — compute and cache on first read, then return the cached value.

  • Migrating a plain field into a controlled access point without breaking callers.

When not to use them
  • For expensive reads — callers may not realise that .area runs a query against a database.

  • When the field is fundamentally a plain data value — keep it a regular property and avoid surprise.

  • For setters with major side effects — call sites look harmless. Prefer an explicit setName(value) method when the cost is non-obvious.

Two halves of one property
A getter and setter share the same property name. Defining them in two object literals — or with two `defineProperty` calls — overwrites the first half. Use one literal with both, or pass both `get` and `set` to one `defineProperty`.
One sentence
`get` and `set` let a property *act like* a field while really running a function — perfect for derivation, validation, and quiet migrations of plain data into computed values.