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.
- 01Write getters and settersSpot
getandsetin objects, classes, and descriptors, and say which one runs on a read or a write. - 02Read their descriptorsTell a getter's descriptor (
get,set) from a data property's descriptor (value,writable). - 03Use them safelyBuild getters that work out values, setters that check values, and run-once getters, without a setter that calls itself.
Properties that run code
In the Property flags & descriptors lesson, a property held a stored value. It also had flags, like writable. We call this kind a data property.
JavaScript has a second kind, the accessor property. From the outside, it looks like any other property. But when you read it or write to it, JavaScript runs a function.
A getter is a function that runs when code reads a property. A setter is a function that runs when code assigns a new value to that property.
You only see one button. Behind it, a machine does the work each time you press it. A getter is the same. user.fullName looks like plain data, but a small function builds the answer each time you read it.
- In real life: Pressing the snack button
- In JavaScript: Reading
user.fullName - In real life: The machine picks the snack and drops it out
- In JavaScript: The getter runs and returns a value
- In real life: The snacks stored behind the glass
- In JavaScript: The stored data the getter uses, like
firstNameandlastName
You saw getters and setters in classes in the Class basics lesson. This lesson goes further. You will learn:
- how to write them in plain objects, not just in classes
- what their descriptors look like
- how a setter can check a new value before it keeps it
- how a getter can work out a value once and save it
- how to keep an old property name working after you rename it
A getter or setter belongs to one property. A later lesson covers Proxy, a tool that can watch every property of an object at once.
Writing get and set
STEP THROUGHInside an object, write get name() for the function that runs on a read. Write set name(value) for the function that runs on a write. The value parameter gets the new value.
Code outside the object never calls them with brackets. It uses normal property code:
- Reading:
user.fullName - Writing:
user.fullName = "Grace Hopper"
Step through the code below and watch which function runs.
Reading fullName runs the getter. Writing to fullName runs the setter with the new 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);Line 14 reads user.fullName. That runs the getter on lines 4 to 6. It joins firstName and lastName and prints Ada Lovelace.
Line 15 writes to user.fullName. That runs the setter on lines 7 to 11. It splits "Grace Hopper" at the space and stores the two parts in firstName and lastName.
So firstName and lastName are data properties. They hold the real text. fullName is the accessor property. It stores nothing itself. Line 17 reads it again, and the getter builds Grace Hopper from the new data.
user.fullName runs the getter, which joins the two names. Writing to it runs the setter, which splits the text into firstName and lastName.A class can have getters and setters too. Here the real name is kept in a private field, #name. A private field is a property that only code inside the class can use.
class User {
#name;
constructor(name) {
this.#name = name;
}
get name() {
return this.#name;
}
set name(value) {
this.#name = value.trim();
}
}
const user = new User("Asha");
user.name = " Ben ";
console.log(user.name);
console.log("#name" in user);The getter and setter for name look the same as in an object. JavaScript puts them on the class's prototype, the shared object every User uses. Line 15 runs the setter, which trims the spaces, so line 16 prints Ben.
Line 17 prints false. #name is not a normal property, so code outside the class can't see it or reach it, not even with user["#name"].
Without a setter, a new value goes straight into the property. With a setter, the value stops at a desk first. There the object can trim text, change units, or refuse a value that makes no sense.
- In real life: A package arrives at the front desk
- In JavaScript: Code runs
sensor.celsius = value - In real life: The person at the desk checks the label
- In JavaScript: The setter checks the value
- In real life: Good packages go to the store room
- In JavaScript: The setter stores the value in
_celsius - In real life: Wrong packages are sent back
- In JavaScript: The setter throws a
RangeError
What the descriptor of a getter looks like
VERIFYA descriptor is a plain object that describes one property. You get it with Object.getOwnPropertyDescriptor. The descriptor shows which kind of property you have:
- A data property has
valueandwritable. - An accessor property has
get,set, or both. - Both kinds have
enumerableandconfigurable.
Read the table one row at a time. Each row asks a question and answers it for both kinds of property.
| Question | Data property | Accessor property |
|---|---|---|
| What is in its descriptor? | value, writable, enumerable, configurable | get, set, enumerable, configurable |
| What happens when you read it? | You get the stored value. | get runs, and you get what it returns. |
| What happens when you write to it? | The new value is stored, if writable is true. | set(value) runs, if there is a setter. |
| When should you use it? | For plain stored data, like firstName. | For values that are worked out, checked, or passed on to another name. |
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);Line 9 gets the descriptor of fullName. Line 10 prints function, because the descriptor has a get function. Lines 11 and 12 print false: there is no value and no writable.
Lines 14 to 20 add a new getter, initials, with Object.defineProperty. Its descriptor has a get function and two flags. Line 21 prints AL. One descriptor can't have both get and value. Pick one kind.
const user = {
first: "Ada",
last: "Lovelace",
get fullName() {
return this.first + " " + this.last;
}
};
class Profile {
constructor(name) {
this.name = name;
}
get greeting() {
return "Hello " + this.name;
}
}
console.log(JSON.stringify(user));
console.log(JSON.stringify(new Profile("Asha")));JSON.stringify turns an object into JSON text. It only includes properties that are both:
- own: on the object itself, not on its prototype
- enumerable: allowed to show up in lists
The getter in user is on the object itself, so line 18 runs it and includes "fullName":"Ada Lovelace". The class getter greeting is on the prototype, so line 19 leaves it out. Reading profile.greeting by name still works.
One more rule for Object.defineProperty: any flag you leave out is false. So a getter defined without enumerable: true still works when you read it. But it won't show up in Object.keys or in JSON. Set the flags you want on purpose.
Work out values and check new ones
INTERACTIVEGetters and setters each have one main job:
- A getter can give a computed value: one worked out from other values, like
fullNamefrom two names. - A setter can validate a new value: check it before storing it. The code still looks like a plain assignment.
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 turns 20°C into 68°F.
Try -500 in the Celsius box. Nothing can be colder than -273.15°C, so the setter refuses it. It throws a RangeError, an error for a value that is out of range, and stores nothing. Now change a name. The fullName getter shows the new name at once, because it reads the current names every time.
celsius goes through the setter first. 25 is stored, but -500 is below the coldest possible temperature, so the setter throws a RangeError.A getter runs on every read. It does not remember its last answer. Step through the code below and count how many times it runs.
Step through two reads and one write. The getter runs on each read. The setter gets the value from the write.
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);Lines 13 and 14 read meter.fahrenheit twice, so the getter runs twice and calls becomes 2. Line 15 writes to the same name, so the setter runs instead. It turns 68°F into 20°C and stores it in _celsius.
A getter looks like a plain property read, so people expect it to be fast and to change nothing. Don't put slow work, saving, or network requests in a getter. Use a method, like save() or buildReport(). The call brackets, (), tell the reader that work is happening.
No setter, a setter that calls itself, and run-once getters
EDGE CASESA property can have a getter but no setter. Then assignment has nowhere to go. What happens depends on the mode:
- Sloppy mode, the normal rules when strict mode is off: JavaScript quietly ignores the assignment.
- Strict mode, the stricter rules turned on by
"use strict": JavaScript throws aTypeError.
The code below shows both, as two separate programs.
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";In the first program, line 6 is ignored, so line 7 still prints read-only. In the second program, line 15 does the same thing in strict mode and throws a TypeError.
A common mistake is a setter that calls itself. Look at line 3 below. Inside set name(value), it runs this.name = value. That is a write to name, so it runs set name again. That run writes to name again, and so on.
const person = { set name(value) { this.name = value; }};person.name = "Ada";A function that calls itself is called recursion. Here it never stops on its own. Each call waits on the call stack, the list of functions that are still running. The list gets too long, and JavaScript stops with a RangeError.
The fix: store the value in a different property, like _name, or in a private field like #name.
_name, so it stores the value once and stops.A getter can also work out a value only once. This is called a lazy getter. On the first read, it works out the value. Then it uses Object.defineProperty to put a plain data property with that value in its own place. Later reads just return the stored value. The getter is gone, so it never runs again.
A lazy getter works out a value once. Then it replaces itself with a plain data property for later 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);Line 4 builds "Weekly report". Lines 5 to 9 replace the getter with a data property that holds that text. Line 10 returns it for this first read. On line 15, the read gets the stored value, and the getter doesn't run.
summary is a getter. The first read runs it once and swaps it for a plain value, so later reads just return "Weekly report".Keeping an old property name working
MIGRATIONSometimes you rename a property that other code already uses. Say oldName becomes newName. Old code still uses oldName. A getter and setter called oldName can keep that code working. This is called a wrapper. It does two things:
- It prints a warning that
oldNameis out of date. - It passes each read and write on to
newName.
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);Line 13 reads oldName. The getter warns, then returns newName. Line 14 writes to oldName. The setter warns, then stores "Grace Hopper" in newName. Line 15 prints Grace Hopper.
Use a wrapper only for a while. Tell people about the new name, warn them when they use the old one, and remove the wrapper once nobody uses oldName.
const account = {
_email: "",
set email(value) {
this._email = value.trim().toLowerCase();
},
get email() {
return this._email;
}
};
account.email = " Asha@Example.COM ";
console.log(account.email);The email setter removes the spaces at both ends with trim(). It makes every letter small with toLowerCase(). Then it stores the result in _email. So line 12 prints asha@example.com, and every email in the object has the same form.
The underscore in _email is only a habit that tells readers "don't use this directly". It doesn't hide anything. In a class, use a private field like #email when the value must stay hidden.
Where you'll use getters and setters
Use a getter or setter when a property name is the right shape, but you need a little code behind it:
- A display name, like
user.fullName, built from two fields. - One value shown in two units, like Celsius and Fahrenheit.
- A check when a value is assigned, like a range or an email format.
- An old property name that still works after a rename.
- A value that is slow to work out, so a lazy getter works it out once.
- A setter on a prototype. Assigning to an object can run a setter found on its prototype (see the Prototype chain lesson).
A profile form is a good place for a setter. account.email = input.value can clean up the email once, so every later part of the code gets the same form. A shopping cart can have a cart.total getter if adding up the items is quick. If the total needs tax from a server, use a method instead. Don't hide a network request behind a property read.
Check where a getter lives before you send an object as JSON. A getter on the object itself can show up in Object.keys and JSON.stringify. A class getter on the prototype usually won't. A property you can read is not always a property that gets sent, so test the JSON text.
user.fullNamebuilt fromfirstNameandlastName- Refuse
sensor.celsius = -500with aRangeError profile.save()sends data to storagearticle.idis already stored and needs no codeoldNamepasses reads on tonewNameafter a rename- Trim and lowercase the email in
account.email = value buildReport()does slow work when askedcreatedAtholds a time that is already known
Put each card in the simplest choice that is honest about the work it does.
Common misconceptions
- "A getter stores its result." No. It runs on every read, unless you save the result yourself, like a lazy getter does.
- "A setter stores the value under its own name." No. A setter just gets the new value. It stores it wherever its code says, like
_celsius. - "Properties that start with an underscore are private." No.
_nameis only a habit. Any code can still read and change it. - "Writing to a getter-only property always throws." Only in strict mode. In sloppy mode, JavaScript ignores the write.
- "Getters and setters are the same as a
Proxy." No. AProxywatches a whole object. A getter or setter belongs to one property. - "A getter is fine for any work." No. Slow or hidden work in a getter surprises people. Use a method for work they should know about.
Practice exercises
5 EXERCISESRun the code and note what it prints. Then change 25 to a temperature below -273.15 and run it again to see the setter refuse it.
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 new value. It writes _celsius only when the value is allowed. The getter returns _celsius, so the code prints 25.
Before you run it, guess the value box.calls prints at the end.
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);The code reads box.label two times, so the getter adds 1 to calls two times. box.calls is 2.
This code fails with a RangeError. Change it so person.name = " Ada " stores the name without the spaces, and reading person.name gives it back.
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 doesn't run set name again. The getter reads _name, so the code prints Ada.
Change this code into an invoice with subtotal: 40 and a total getter that adds 20% tax. What number does the descriptor's value show?
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 works out 40 × 1.2 = 48. It then replaces the getter with a data property that holds 48. The descriptor's value is 48.
Keep oldName working while the real property is now newName. After the assignment, what name does the code print?
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 the getter and the setter print a warning and pass the work on to newName. Old code keeps working, and people see the warning.
Check your understanding
7 QUESTIONSQuestion 1 of 7Which descriptor belongs to an accessor property (a property with a getter or setter)?
Choose an answer to see the explanation.
Question 2 of 7This code reads a getter once. What does it 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 7Inside
set name(value), why should the setter store the value in_nameand not inname?Choose an answer to see the explanation.
Question 4 of 7This code writes 68 through a setter. What does it 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 7In sloppy mode (strict mode off), code writes to a property that has a getter but no setter. What happens?
Choose an answer to see the explanation.
Question 6 of 7What is a wrapper (a getter and setter for an old property name) good for?
Choose an answer to see the explanation.
Question 7 of 7This code reads the same getter twice. What does it 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
- Reading an accessor property runs its getter. Writing to it runs its setter.
- A getter's descriptor has
getandset, notvalueandwritable. - A setter should check the value first, then store it in a different property, like
_name. Writing to its own name makes it call itself. - Keep getters quick, with no surprises. Use a method for work people should notice.
- A lazy getter works out a value once. A wrapper keeps an old name working for a while.
An accessor property is a property that runs a function when you read it, write to it, or both.
Up next: Freezing, sealing & immutability.