Property getters and setters

Up to now, every property you’ve put on an object has stored a value. You write user.name = "Maya", and later user.name hands that same string back. Nothing runs in between. That’s one kind of property.

There’s a second kind that behaves very differently under the hood while looking identical from the outside.

JavaScript splits object properties into two families:

  • Data properties — the ordinary ones. They hold a value. Reading gives it back, writing replaces it. Everything you’ve used so far belongs here.
  • Accessor properties — the new idea in this chapter. They don’t store a value at all. Instead, they run a function when you read them and (optionally) another function when you write them. To the code touching them, they still look like a plain property.
Data property
read obj.x
stored value comes back
Accessor property
read obj.x
getter function runs
whatever it returns comes back
A read hits a data property directly, but routes through a function for an accessor

Getters and setters

An accessor property is built from two methods: a getter that runs when the property is read, and a setter that runs when it’s assigned. In an object literal you mark them with the get and set keywords in front of a method name:

let obj = {
  get propName() {
    // runs when obj.propName is read
  },

  set propName(value) {
    // runs when obj.propName = value is executed
  }
};

The getter fires on a read (obj.propName). The setter fires on a write (obj.propName = ...), and the value on the right of the = arrives as its single argument.

Here’s where it earns its keep. Say you have a user with a name and a surname:

let user = {
  name: "Maya",
  surname: "Vance"
};

You’d like a fullName of "Maya Vance". Storing it as a third data property means copying information you already have, and it goes stale the moment name changes. A getter computes it on demand instead:

let user = {
  name: "Maya",
  surname: "Vance",

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

alert(user.fullName); // Maya Vance

Notice the call site. You write user.fullName, not user.fullName(). No parentheses. From the outside it reads like any ordinary property, and that’s the whole point — the function running behind it is invisible to the code doing the read.

user.fullName
→ runs →
get fullName()
return ${name} ${surname}
“Maya Vance”
Reading user.fullName triggers the getter, which reads name and surname through this

Right now fullName has only a getter. Try to assign to it and JavaScript complains, because there’s no setter to receive the value:

let user = {
  get fullName() {
    return `...`;
  }
};

user.fullName = "Test"; // Error (property has only a getter)

Add a setter and the property becomes writable too. This setter takes the incoming string, splits it on the space, and stores the two halves back into name and surname:

let user = {
  name: "Maya",
  surname: "Vance",

  get fullName() {
    return `${this.name} ${this.surname}`;
  },

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  }
};

// the setter runs with value = "Nadia Cross"
user.fullName = "Nadia Cross";

alert(user.name);    // Nadia
alert(user.surname); // Cross

You now have a virtual property. fullName occupies no storage of its own — reading it composes two other fields, writing it decomposes back into them — yet it reads and writes exactly like a normal property.

user.fullName = “Nadia Cross”

↓ set fullName(value) runs with value = “Nadia Cross”
↓ value.split(“ “) → [“Nadia”, “Cross”]
name: “Nadia”
surname: “Cross”
Writing fullName runs the setter, which splits the string back into name and surname

Try both directions live. Reading fullName composes the two stored fields; writing it splits your input back into name and surname. Edit the code and re-run to prove fullName never stores anything itself.

interactiveA two-way fullName accessor

Accessor descriptors

Back in the chapter on property flags and descriptors, you saw that every property carries hidden attributes you can inspect and change with Object.getOwnPropertyDescriptor and Object.defineProperty. Accessor properties have descriptors too, but the shape is different.

A data property’s descriptor has value and writable. An accessor property has neither. In their place sit get and set functions. So an accessor descriptor can hold:

  • get — a function taking no arguments, run when the property is read.
  • set — a function taking one argument, run when the property is assigned.
  • enumerable — whether the property shows up in loops, same meaning as for data properties.
  • configurable — whether the property can be deleted or reconfigured, same meaning as for data properties.
Data descriptor
value
writable
enumerable
configurable
Accessor descriptor
get
set
enumerable
configurable
The two descriptor shapes are mutually exclusive

The same fullName accessor, defined with Object.defineProperty instead of literal syntax, looks like this:

let user = {
  name: "Maya",
  surname: "Vance"
};

Object.defineProperty(user, 'fullName', {
  get() {
    return `${this.name} ${this.surname}`;
  },

  set(value) {
    [this.name, this.surname] = value.split(" ");
  }
});

alert(user.fullName); // Maya Vance

for (let key in user) alert(key); // name, surname

The for..in loop only prints name and surname here. Since fullName was defined with defineProperty and no enumerable: true, its enumerable flag defaults to false, so the loop skips it. (Had you written fullName directly in the object literal, it would be enumerable and would show up.)

A property is one kind or the other, never both. It either stores a value or it computes one through accessors. Mixing the two in a single descriptor is rejected:

// Error: Invalid property descriptor.
Object.defineProperty({}, 'prop', {
  get() {
    return 1;
  },

  value: 2
});

Smarter getters and setters

A setter is a checkpoint. Every write to the property flows through your function, which means you get to inspect, reject, or transform the value before anything is stored. That makes accessors a natural place for validation.

Suppose a user’s name must be at least four characters. Wrap the real storage in an accessor pair: keep the actual value in a backing property _name, and guard writes through set name:

let user = {
  get name() {
    return this._name;
  },

  set name(value) {
    if (value.length < 4) {
      alert("Name is too short, need at least 4 characters");
      return;
    }
    this._name = value;
  }
};

user.name = "Rajan";
alert(user.name); // Rajan

user.name = ""; // Name is too short...

The value lives in _name. Every read and write to name passes through the getter and setter, so the length check can’t be bypassed by ordinary code assigning user.name.

user.name = “…”
set name(value)
length < 4 ? reject
→ if ok →
_name
“…”
The public name accessor guards the private _name that actually holds the value

Here the setter is a working checkpoint. Type a name and assign it: anything shorter than four characters is rejected before it ever reaches _name, and the stored value stays put. Try "", abc, then Rajan.

interactiveA setter that validates every write

Using accessors for compatibility

Accessors give you a quiet way to change how a property works without breaking the code that reads it. You can take a plain data property that’s been in use for a while and swap it for a computed one, and callers never notice.

Picture user objects that started life with name and age as ordinary data properties:

function User(name, age) {
  this.name = name;
  this.age = age;
}

let maya = new User("Maya", 25);

alert( maya.age ); // 25

Time passes, and you realize storing age was the wrong call. An age drifts out of date every birthday; a birthday doesn’t. So you switch the stored field:

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;
}

let maya = new User("Maya", new Date(1992, 6, 1));

Now the trouble. Plenty of existing code still reads maya.age, and that code might live in other files, other teams, other projects you can’t easily edit. Hunting down every age read is slow and risky. And age is genuinely handy to have around.

So keep it — as a computed accessor built on top of birthday:

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;

  // age is derived from birthday and today's date
  Object.defineProperty(this, "age", {
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    }
  });
}

let maya = new User("Maya", new Date(1992, 6, 1));

alert( maya.birthday ); // birthday is available
alert( maya.age );      // ...and so is age, computed on the fly

Old code asking for maya.age still works — it just gets a freshly computed number instead of a stored one. New code can use the more precise birthday. The migration happens under the property name, invisibly.

before
age: 25 (stored)
after
birthday: Date(1992, 6, 1) (stored)
get age() → thisYear − 1992
age stops being stored and becomes a live calculation over the stored birthday

Only birthday is stored below, yet age reads back as an ordinary property. Change the birth year and re-read: the age recomputes against today’s date every single time, so it can never go stale.

interactiveage computed from a stored birthday

Summary at a glance

  • Data property: stores a value. Descriptor uses value + writable.
  • Accessor property: runs functions. Descriptor uses get + set. Never both kinds at once.
  • A getter runs on read and takes no arguments; a setter runs on write and takes the assigned value as its one argument.
  • You define them with get/set in an object literal, or with get()/set() in an Object.defineProperty descriptor.
  • Getter-only means read-only: assignment throws in strict mode, fails silently otherwise.
  • Setters are the natural home for validation; getters are the natural home for computed and derived values.
  • Because an accessor looks like a plain property from outside, you can retrofit one over an existing data property to change behavior without touching the code that uses it.