Skip to content
JavaScript js objects 4 min read

Getters & Setters

Most object properties are plain data slots: you write a value and read it back unchanged. Getters and setters break that one-to-one rule by letting a function run whenever a property is read or written. This is how you expose computed (derived) values that always stay in sync, and how you intercept assignments to validate or transform incoming data — all while keeping the natural obj.property syntax callers already know.

Accessor syntax in object literals

Inside an object literal you declare an accessor with the get and set keywords followed by a property name. A getter takes no parameters and returns a value; a setter takes exactly one parameter (the value being assigned) and returns nothing useful. To callers, the accessor looks like an ordinary property — there are no parentheses.

const user = {
  firstName: "Ada",
  lastName: "Lovelace",

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

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

console.log(user.fullName);      // read → runs the getter
user.fullName = "Grace Hopper"; // assign → runs the setter
console.log(user.firstName);
console.log(user.lastName);

Output:

Ada Lovelace
Grace
Hopper

Notice the getter is invoked without () and the setter fires on plain assignment. The accessor reads this, so it always reflects the object’s current state.

Computed and derived properties

The most common use of a getter is exposing a value derived from other properties. Because the function runs on every read, the result is never stale — there is no cached field to forget to update.

const cart = {
  items: [
    { name: "Keyboard", price: 80, qty: 1 },
    { name: "Mouse", price: 25, qty: 2 },
  ],

  get total() {
    return this.items.reduce((sum, i) => sum + i.price * i.qty, 0);
  },

  get itemCount() {
    return this.items.reduce((sum, i) => sum + i.qty, 0);
  },
};

console.log(`${cart.itemCount} items, $${cart.total}`);
cart.items.push({ name: "Pad", price: 15, qty: 1 });
console.log(`${cart.itemCount} items, $${cart.total}`);

Output:

3 items, $130
4 items, $145

Tip: Getters should be cheap and side-effect free. Callers expect reading a property to be instant, so avoid heavy computation or network calls inside a getter — use a regular method when work is involved.

Validation on set

A setter is the natural place to guard against bad data. By storing the real value in a separate “backing” property (conventionally prefixed with _) you can reject or coerce values before they land.

const account = {
  _balance: 0,

  get balance() {
    return this._balance;
  },

  set balance(amount) {
    if (typeof amount !== "number" || Number.isNaN(amount)) {
      throw new TypeError("balance must be a number");
    }
    if (amount < 0) {
      throw new RangeError("balance cannot be negative");
    }
    this._balance = amount;
  },
};

account.balance = 250;
console.log(account.balance);

try {
  account.balance = -10;
} catch (err) {
  console.log(err.message);
}

Output:

250
balance cannot be negative

The _balance field is still publicly reachable; the underscore is only a convention. For true privacy, pair accessors with #private class fields, where the backing store is genuinely inaccessible from outside.

Object.defineProperty accessors

Accessors are not limited to literal syntax. Object.defineProperty (and Object.defineProperties) can attach a getter/setter to an existing object via a descriptor with get and set functions. This is useful when you build objects dynamically or want to control the descriptor flags.

const temperature = { celsius: 20 };

Object.defineProperty(temperature, "fahrenheit", {
  get() {
    return this.celsius * 1.8 + 32;
  },
  set(value) {
    this.celsius = (value - 32) / 1.8;
  },
  enumerable: true,
  configurable: true,
});

console.log(temperature.fahrenheit);
temperature.fahrenheit = 212;
console.log(temperature.celsius);

Output:

68
100

An accessor descriptor uses get/set and must not also specify value or writable — those belong to data descriptors, and mixing them throws a TypeError.

Data vs accessor properties

AspectData propertyAccessor property
Defined withvalueget and/or set
Stores a value directlyYesNo (computed each time)
Controls writabilitywritable: true/falsePresence/absence of a set
Read-only whenwritable: falseOnly a get, no set
Runs code on accessNoYes

A getter with no matching setter is effectively read-only: assigning to it does nothing in non-strict mode and throws in strict mode.

Best Practices

  • Keep getters fast, pure, and free of observable side effects — treat them like the property they impersonate.
  • Use setters to validate, coerce, or normalize input rather than to trigger far-reaching side effects.
  • When a getter and setter share state, store it in a clearly named backing field (_value) or a #private class field.
  • Prefer literal get/set syntax for clarity; reach for Object.defineProperty only when building objects dynamically or tuning descriptor flags.
  • Omit the setter to make a property read-only, and remember that doing so throws on assignment in strict mode.
  • Document derived getters so callers know the value is computed, not stored.
Last updated June 1, 2026
Was this helpful?