Skip to Content
JavaScript Classes With Getters and Setters
Intermediate
0 likes
Tutorials

JavaScript Classes With Getters and Setters

Getters compute a value when you read it, so totals never go stale. Setters check a value when you assign it, so a negative price throws instead of saving. With private fields, nobody can go around them.

A getter lets you read a computed value like a normal property: order.total instead of order.getTotal(). A setter lets you check a value when someone assigns it: product.price = -5 can throw instead of quietly saving a negative price. Together with private fields, they keep an object's data valid without making the code that uses it any longer.

Skip ahead: If you only need the syntax, skip to "The complete pattern".

Start with the problem

const product = { name: "Coffee", price: 180 };
product.price = -5;
product.price = "free";
console.log(product);
{ name: 'Coffee', price: 'free' }

Plain objects accept anything. If prices come from a form, an import or another developer's code, sooner or later one is wrong, and you find out when a total says NaN.

A getter: computed on read

class Product {
  constructor(name, price, vatRate = 0.12) {
    this.name = name;
    this.price = price;
    this.vatRate = vatRate;
  }

  get priceWithVat() {
    return Math.round(this.price * (1 + this.vatRate) * 100) / 100;
  }
}

const coffee = new Product("Coffee", 180);
console.log(coffee.priceWithVat);
coffee.price = 200;
console.log(coffee.priceWithVat);
201.6
224

No brackets when you read it. And because it's calculated every time, it can't go stale when price changes. If you'd stored priceWithVat in the constructor, it would still say 201.6 after the price changed.

A setter: checked on write

To check a value on the way in, the class needs somewhere private to keep the real value, and a setter in front of it:

class Product {
  #price = 0;

  constructor(name, price) {
    this.name = name;
    this.price = price; // goes through the setter below
  }

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

  set price(value) {
    if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
      throw new RangeError(`Price must be a number of 0 or more, got ${JSON.stringify(value)}`);
    }
    this.#price = value;
  }
}

const coffee = new Product("Coffee", 180);
coffee.price = 200;
console.log(coffee.price);

for (const bad of [-5, "free", NaN]) {
  try {
    coffee.price = bad;
  } catch (error) {
    console.log(error.message);
  }
}
console.log(coffee.price);
200
Price must be a number of 0 or more, got -5
Price must be a number of 0 or more, got "free"
Price must be a number of 0 or more, got null
200

Notice:

  • The constructor assigns this.price, which runs the setter, so even new Product("Coffee", -5) is rejected. Validate in one place.
  • The real value lives in #price. A private field can't be reached from outside the class, so nobody can skip the setter.
  • JSON.stringify(NaN) prints null, which is why the last message says got null. Good to know when you build error messages.

⚠ Warning: Don't name the stored field the same as the getter without the #. set price(v) { this.price = v; } calls the setter again from inside itself, forever, and you get RangeError: Maximum call stack size exceeded. The stored value needs a different name, and #price is the clean choice.

A getter with no setter is read-only

If you define only get, assignment can't change it. In an ES module (strict mode, which is what you get with import/export or a .mjs file) the assignment throws:

class Order {
  #items = [{ price: 180, qty: 2 }];
  get total() {
    return this.#items.reduce((sum, item) => sum + item.price * item.qty, 0);
  }
}

const order = new Order();
try {
  order.total = 0;
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
}
console.log(order.total);
TypeError: Cannot set property total of #<Order> which has only a getter
360

In an old-style script without strict mode, the same assignment is silently ignored and total stays 360. Either way, the value can't be overwritten. That's what you want for derived values. A total should change because the items changed, never by assignment.

The complete pattern

class Thing {
  #value;

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

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

  set value(next) {
    // check next, throw if invalid
    this.#value = next;
  }
}

When not to bother

  • Plain data that only moves from an API to the screen doesn't need setters. Validate once at the edge, when it comes in.
  • Expensive getters run on every read. If total loops over 10,000 items and the screen reads it 50 times, compute it once and keep it in a variable.
  • Getters with side effects (saving, logging, fetching) surprise everyone. Reading a property should never change anything.

💡 Tip: JSON.stringify skips getters and private fields. If you send these objects to an API, add a toJSON() method that returns exactly the fields you want, for example return { name: this.name, price: this.price };.

Which values in your code have rules (never negative, never empty, only certain statuses), and where are those rules checked today?

About

Write getters for computed values, setters that reject bad values, and private fields so the checks can't be skipped, plus the infinite-loop trap. After this, invalid data gets stopped at the door.

Responsible
Jay P
Last Update
08/30/2026
Reading Time
4 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.