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
| Aspect | Data property | Accessor property |
|---|---|---|
| Defined with | value | get and/or set |
| Stores a value directly | Yes | No (computed each time) |
| Controls writability | writable: true/false | Presence/absence of a set |
| Read-only when | writable: false | Only a get, no set |
| Runs code on access | No | Yes |
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#privateclass field. - Prefer literal
get/setsyntax for clarity; reach forObject.definePropertyonly 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.