Getters & setters
Learn how JavaScript accessor properties run functions on reads and writes, how their descriptors differ from data properties, and how to use them for computed values, validation, caching, and compatibility wrappers.
- 01Read accessor syntaxRecognize
getandsetin object literals, classes, and descriptors. - 02Inspect descriptorsTell accessor descriptors from data descriptors without mixing their fields.
- 03Use accessors safelyBuild computed, validated, lazy, and compatibility properties without recursion traps.
Properties that run code
In the Property flags & descriptors lesson, a property was mostly a box: a name with a stored value plus flags like writable. JavaScript has a second kind of property: an accessor property. It looks like a normal property from the outside, but reading or writing it calls a function.
A getter is a function JavaScript calls when code reads a property; a setter is a function JavaScript calls when code assigns to that property.
The label on the machine is simple, but machinery sits behind it. A getter works the same way: user.fullName reads like data, while a small function assembles the answer fresh from current data.
- In real life: Pressing the snack button
- In JavaScript: Reading
user.fullName - In real life: The machine assembles and drops the snack
- In JavaScript: The getter runs and returns a value
- In real life: Stock behind the glass
- In JavaScript: Backing data such as
firstNameandlastName
Where the analogy stops: A real vending machine may take money and make noise. A good getter should usually be cheap, predictable, and free of surprising side effects.
You have seen class getters and setters in Class basics. This lesson goes deeper: plain objects, descriptors, validation, lazy caching, and wrappers that keep old property names working during an API migration. The later Proxy lesson covers intercepting all property access; accessors are narrower and live on one property at a time.
get & set syntax
STEP THROUGHIn an object literal, write get name() for the read function and set name(value) for the write function. Code outside the object does not call them with parentheses. It uses property syntax: user.fullName and user.fullName = "Grace Hopper".
Reading fullName calls a getter. Assigning to fullName calls a setter with the assigned value.
script
firstName: "Ada", lastName: "Lovelace", get fullName() { return this.firstName + " " + this.lastName; }, set fullName(value) { const parts = value.split(" "); this.firstName = parts[0]; this.lastName = parts[1]; }}; console.log(user.fullName);user.fullName = "Grace Hopper";console.log(user.firstName);console.log(user.fullName);Notice the important split: firstName and lastName are data properties. fullName is the accessor. The setter changes the backing data, and the getter recomputes from that backing data the next time somebody reads the property.
Assignment is not always a blind drop into a box. With a setter, the value reaches a checkpoint first. The object can trim text, convert units, or reject impossible values before it accepts the delivery.
- In real life: A package arrives at the front desk
- In JavaScript: Code assigns
sensor.celsius = value - In real life: The receptionist checks the label
- In JavaScript: The setter validates the value
- In real life: Accepted packages go to storage
- In JavaScript: The setter writes backing data such as
_celsius - In real life: Bad deliveries are refused
- In JavaScript: The setter throws a
RangeError
Where the analogy stops: A receptionist can ask follow-up questions. A setter receives one assigned value and must decide from that value and the object’s current state.
Accessor descriptors
VERIFYDescriptors reveal the difference between the two property kinds. A data descriptor has value and writable. An accessor descriptor has get and/or set. Both kinds can still have enumerable and configurable.
| Question | Data property | Accessor property |
|---|---|---|
| Descriptor fields | value, writable, enumerable, configurable | get, set, enumerable, configurable |
| Reading | Returns the stored value | Calls get and returns what it returns |
| Writing | Stores a new value when writable is true | Calls set(value) when a setter exists |
| Best for | Plain state such as firstName | Computed, validated, logged, or compatibility names |
const account = { firstName: "Ada", lastName: "Lovelace", get fullName() { return this.firstName + " " + this.lastName; }}; const descriptor = Object.getOwnPropertyDescriptor(account, "fullName");console.log(typeof descriptor.get);console.log("value" in descriptor);console.log("writable" in descriptor); Object.defineProperty(account, "initials", { get() { return this.firstName[0] + this.lastName[0]; }, enumerable: true, configurable: true});console.log(account.initials);The descriptor for fullName contains a get function. It does not contain value or writable. If you define an accessor with Object.defineProperty, give it get and/or set, plus the flags you need. Do not mix accessor fields with data fields in the same descriptor.
Computed & validated properties
INTERACTIVEComputed properties are the friendliest use of getters: the value is derived from other values. Validated properties are the friendliest use of setters: assignment still looks simple, but invalid data never enters the object.
const profile = { firstName: "Ada", lastName: "Lovelace", _celsius: 20, get fullName() { return this.firstName + " " + this.lastName; }, set celsius(value) { if (value < -273.15) { throw new RangeError("Temperature cannot go below absolute zero."); } this._celsius = value; }, get fahrenheit() { return this._celsius * 9 / 5 + 32; }};The fullName getter now returns "Ada Lovelace". The fahrenheit getter returns 68 from 20°C.
Try -500 in the Celsius input. The setter refuses it with a RangeError message instead of storing impossible state. Try changing the names: the fullName getter updates immediately because it reads current backing data each time.
Step through two property reads and one assignment. The getter runs on each read; the setter receives the assigned value.
script
calls: 0, _celsius: 20, get fahrenheit() { this.calls += 1; return this._celsius * 9 / 5 + 32; }, set fahrenheit(value) { this._celsius = (value - 32) * 5 / 9; }}; console.log(meter.fahrenheit);console.log(meter.fahrenheit);meter.fahrenheit = 68;console.log(meter._celsius);console.log(meter.calls);Because a getter looks like a plain property read, keep it cheap and mostly pure. If reading it saves data, makes a network request, or performs a slow calculation every time, prefer a method such as save() or buildReport() so callers can see that work is happening.
Getter-only, recursion & lazy getters
EDGE CASESIf a property has a getter but no setter, assignment cannot update it. In sloppy mode, JavaScript silently ignores that assignment. In strict mode, it throws a TypeError. The code below separates the two checks so tests can prove both behaviors.
const settings = { get mode() { return "read-only"; }};settings.mode = "edit";console.log(settings.mode); "use strict";const settings = { get mode() { return "read-only"; }};settings.mode = "edit";The classic trap is recursion. Inside set name(value), assigning this.name = value calls the same setter again, which calls it again, until the call stack overflows with a RangeError. Use a different backing property such as _name or a private field.
const person = { set name(value) { this.name = value; }};person.name = "Ada";Accessors can also be used for lazy values. A getter computes a value on the first read, then uses Object.defineProperty to replace itself with a data property. Future reads are fast and do not call the getter.
A lazy getter can compute once, then redefine itself as an ordinary data property for future reads.
script
title: "Weekly", get summary() { const value = this.title + " report"; Object.defineProperty(this, "summary", { value, enumerable: true, configurable: true }); return value; }}; console.log(report.summary);console.log(report.summary);console.log(Object.getOwnPropertyDescriptor(report, "summary").value);Compatibility wrappers
MIGRATIONAccessors are useful during migrations. Suppose an API moves from oldName to newName. A wrapper can keep old code running, warn developers, and forward reads and writes to the new property.
const user = { newName: "Ada Lovelace", get oldName() { console.warn("oldName is deprecated; use newName"); return this.newName; }, set oldName(value) { console.warn("oldName is deprecated; use newName"); this.newName = value; }}; console.log(user.oldName);user.oldName = "Grace Hopper";console.log(user.newName);This is not a reason to hide major behavior behind a property forever. It is a bridge: document the new name, warn on the old name, and eventually remove the wrapper when callers have migrated.
Where you’ll use accessors
Reach for accessors when the property shape is valuable but you need logic:
- Display names such as
user.fullNamefrom separate fields. - Unit conversion such as Celsius and Fahrenheit views of the same value.
- Validation at assignment time for ranges, formats, or normalized text.
- Compatibility aliases while a public object changes shape.
- Lazy cached values that are expensive once but cheap afterward.
- Inherited setters from the Prototype chain, where assignment on one object can be intercepted by a setter found higher up.
user.fullNameassembled fromfirstNameandlastName- Reject
sensor.celsius = -500with aRangeError profile.save()sends data to storagearticle.idis already stored and needs no logicoldNameforwards tonewNameduring a migration- Trim and lowercase
account.email = value buildReport()performs expensive work on demandcreatedAtstores the timestamp already known
Sort each design choice by the simplest API that tells the truth about the work being done.
Common misconceptions
- “A getter stores its result.” Not automatically. It runs every time unless you cache intentionally.
- “A setter makes a property writable like data.” A setter receives assignments; there may still be no stored value named after the accessor.
- “Underscore properties are private.”
_nameis a convention, not protection. - “Getter-only assignment always throws.” It throws in strict mode; sloppy mode ignores it.
- “Accessors are the same as Proxies.” Proxies intercept many operations for an object; accessors belong to specific properties.
- “Getters are great for anything automatic.” Hidden expensive work surprises callers. Prefer methods for visible actions.
Practice exercises
5 EXERCISESRun the code, then try changing the assignment to an impossible temperature. What does the valid version print?
const sensor = {
_celsius: 0,
set celsius(value) {
if (value < -273.15) throw new RangeError("too cold");
this._celsius = value;
},
get celsius() {
return this._celsius;
}
};
sensor.celsius = 25;
console.log(sensor.celsius);const sensor = {
_celsius: 0,
set celsius(value) {
if (value < -273.15) throw new RangeError("too cold");
this._celsius = value;
},
get celsius() {
return this._celsius;
}
};
sensor.celsius = 25;
console.log(sensor.celsius);The setter checks the incoming value, writes _celsius only when it is valid, and the getter returns the backing data.
Before running it, predict the final value of box.calls.
const box = {
calls: 0,
get label() {
this.calls += 1;
return "box";
}
};
console.log(box.label);
console.log(box.label + "!");
console.log(box.calls);const box = {
calls: 0,
get label() {
this.calls += 1;
return "box";
}
};
console.log(box.label);
console.log(box.label + "!");
console.log(box.calls);There are two reads of box.label, so the getter increments calls twice.
The starter overflows the stack. Rewrite it so assigning person.name = " Ada " stores a trimmed name.
const person = {
set name(value) {
this.name = value;
}
};
person.name = "Ada";const person = {
_name: "",
set name(value) {
this._name = value.trim();
},
get name() {
return this._name;
}
};
person.name = " Ada ";
console.log(person.name);The setter writes _name, so it does not trigger set name again. The getter reads _name.
Adapt the pattern for an invoice total. What number appears as the cached descriptor value?
const report = {
title: "Weekly",
get summary() {
const value = this.title + " report";
Object.defineProperty(this, "summary", {
value,
enumerable: true,
configurable: true
});
return value;
}
};
console.log(report.summary);
console.log(report.summary);
console.log(Object.getOwnPropertyDescriptor(report, "summary").value);const invoice = {
subtotal: 40,
get total() {
const value = this.subtotal * 1.2;
Object.defineProperty(this, "total", { value, configurable: true });
return value;
}
};
console.log(invoice.total);
console.log(Object.getOwnPropertyDescriptor(invoice, "total").value);The first read computes and replaces the accessor with a data property. The descriptor proves the cached value is 48.
Keep oldName working while the real property becomes newName. What new name prints after assignment?
const account = {
newName: "Ada",
get oldName() {
console.warn("oldName is deprecated; use newName");
return this.newName;
},
set oldName(value) {
console.warn("oldName is deprecated; use newName");
this.newName = value;
}
};
account.oldName = "Grace";
console.log(account.newName);const account = {
newName: "Ada",
get oldName() {
console.warn("oldName is deprecated; use newName");
return this.newName;
},
set oldName(value) {
console.warn("oldName is deprecated; use newName");
this.newName = value;
}
};
account.oldName = "Grace";
console.log(account.newName);Both accessor functions warn and forward to newName, so old callers keep working while migration messages appear.
Check your understanding
7 QUESTIONSQuestion 1 of 7Which descriptor shape belongs to an accessor property?
Choose an answer to see the explanation.
Question 2 of 7What does this getter example print?
Read the code, then predictconst user = { first: "Ada", last: "Lovelace", get name() { return this.first + " " + this.last; } }; console.log(user.name);Choose an answer to see the explanation.
Question 3 of 7Why should a setter usually write to
_nameinstead ofnameinsideset name(value)?Choose an answer to see the explanation.
Question 4 of 7What does this temperature setter print?
Read the code, then predictconst meter = { _c: 0, get f() { return this._c * 9 / 5 + 32; }, set f(value) { this._c = (value - 32) * 5 / 9; } }; meter.f = 68; console.log(meter._c);Choose an answer to see the explanation.
Question 5 of 7A getter-only property is assigned in sloppy mode. What happens?
Choose an answer to see the explanation.
Question 6 of 7What is a good use for a compatibility wrapper?
Choose an answer to see the explanation.
Question 7 of 7What do these repeated getter reads print?
Read the code, then predictconst item = { calls: 0, get code() { this.calls += 1; return "A"; } }; console.log(item.code + item.code); console.log(item.calls);Choose an answer to see the explanation.
Key takeaways
- Getter reads and setter assignments look like property access but call functions.
- Accessor descriptors use
get/set, notvalue/writable. - Write setters to backing data, validate before storing, and avoid assigning the accessor from inside itself.
- Keep getters cheap and predictable; use methods for work callers should notice.
- Wrappers and lazy getters are practical patterns when used deliberately.
A JavaScript accessor property is a property whose read and/or write operation is powered by functions.
Up next: Freezing, sealing & immutability.