Getters and setters in a class
A getter and a setter are a pair of special class methods that run automatically on a read (obj.prop) and on an assignment (obj.prop = value). From the outside the property looks like an ordinary field, but there is code behind it that can validate, compute or log.
Theory
TL;DR
get name()fires whenobj.nameis read,set name(value)fires whenobj.name = valueis assigned.- It is an accessor property: no value is stored in it, the value is computed or delegated to another field.
- Getter only means a read only property; setter only means a write only property (reading returns
undefined). - They pair perfectly with
#fieldprivate data: the data is protected and the access is controlled. - The same mechanism is available outside classes through
Object.definePropertyor an object literal.
Quick example
class User {
constructor(name) {
this._name = name; // a "private" field by convention
}
get name() {
console.log('getter called');
return this._name;
}
set name(value) {
console.log('setter called');
if (value.length < 2) throw new Error('Name is too short');
this._name = value;
}
}
const u = new User('Maria');
console.log(u.name); // 'getter called', then 'Maria'
u.name = 'Oleh'; // 'setter called'
console.log(u.name); // 'Oleh'The call syntax stays exactly like a plain field: no parentheses, no getName().
How it works under the hood
get and set in a class body are shorthand for an accessor property descriptor. Formally the class does roughly what Object.defineProperty does:
const user = {
_name: 'Maria'
};
Object.defineProperty(user, 'name', {
get() {
return this._name.toUpperCase();
},
set(value) {
this._name = value.trim();
}
});
user.name = ' oleh ';
console.log(user.name); // 'OLEH'Important consequences:
- The accessor lives on the class prototype, not on the instance, so it is not duplicated in every object.
- It is non enumerable (
enumerable: false), so it does not appear inObject.keys(instance)or in theJSON.stringify(instance)output. If a computed value must be in the JSON, add atoJSON()method. - The descriptor can be inspected:
Object.getOwnPropertyDescriptor(User.prototype, 'name')returns an object withgetandsetfields. - An accessor can be static:
static get version() { return '1.0'; }is read asUser.version.
Getter without a setter, and setter without a getter
With only a getter defined, the property becomes read only. It is typically used for computed values: area, fullName, priceWithTax.
class Circle {
constructor(radius) {
this.radius = radius;
}
get area() {
return Math.PI * this.radius ** 2;
}
}
const c = new Circle(10);
console.log(c.area); // 314.159...
c.area = 100; // in a module or in strict mode: TypeError
console.log(c.area); // 314.159..., the value did not changeIn sloppy mode such an assignment is silently ignored, while in modules and in strict mode it throws
TypeError: Cannot set property area. Class code is always strict, but the assignment line itself runs in its own context, so the behaviour depends on that context.
The mirror case, a setter without a getter, is handy when you only need to react to a write and recompute internal data.
class Temperature {
set celsius(value) {
this.fahrenheit = value * 1.8 + 32;
}
}
const t = new Temperature();
t.celsius = 25;
console.log(t.fahrenheit); // 77
console.log(t.celsius); // undefined, there is no getterWorking with private fields
Most often a getter and a setter are a facade over a #field private property. The data is unreachable from outside, but the class decides how to expose it and what to accept.
class Account {
#balance = 0;
get balance() {
return this.#balance;
}
set balance(value) {
if (value < 0) throw new Error('Balance cannot be negative');
this.#balance = value;
}
}
const acc = new Account();
acc.balance = 100;
console.log(acc.balance); // 100
// acc.balance = -50; // Error: Balance cannot be negativeThis gives you the main benefit of accessors: the public contract (acc.balance) stays stable while the internal storage can change at any time.
Computed properties
A getter together with a setter lets you split a composite value into parts and assemble it back.
class User {
constructor(first, last) {
this.first = first;
this.last = last;
}
get fullName() {
return `${this.first} ${this.last}`;
}
set fullName(value) {
const [first, last] = value.split(' ');
this.first = first;
this.last = last;
}
}
const u = new User('Maria', 'Koval');
console.log(u.fullName); // 'Maria Koval'
u.fullName = 'Oleh Shevchenko';
console.log(u.first); // 'Oleh'
console.log(u.last); // 'Shevchenko'The fullName property does not exist as a field in memory: it is recomputed from first and last every time, so the data can never drift out of sync.
Comparison with a plain property
| Feature | Plain property | Getter or setter |
|---|---|---|
| Stores a value | yes | no, it computes or delegates |
| Runs code on access | no | yes, automatically |
| Can add validation or logic | no | yes |
| Works with private fields | yes | yes |
| Access syntax | obj.prop | obj.prop, identical |
| Lives on the instance | yes | on the class prototype |
Included in JSON.stringify | yes | no |
Common mistakes
- Calling it like a method.
u.name()throwsTypeError: u.name is not a function, because a getter returns a value, not a function. - Infinite recursion. Writing
this.name = valueinsideset name(value)makes the setter call itself and blow the stack. You need separate storage:this._nameor#name. - Same name for the accessor and a field.
get value()together with a class fieldvalue = 1does not work: the instance field shadows the prototype accessor. - Expecting accessors in JSON. Computed properties are not serialised, because they are non enumerable and live on the prototype.
- Heavy computation in a getter. Reading a property looks cheap, so a call inside a loop or a render easily becomes a bottleneck; cache the result.
- Side effects in a getter. Network requests, state mutations and logging make the code unpredictable: a getter should only return a value.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.