cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

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.

By the end you can
  • 01
    Read accessor syntaxRecognize get and set in object literals, classes, and descriptors.
  • 02
    Inspect descriptorsTell accessor descriptors from data descriptors without mixing their fields.
  • 03
    Use 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.

One-sentence definition

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.

Real-life analogyA getter is a vending machine behind a property name

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 firstName and lastName

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 THROUGH

In 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".

Read a getter, assign to a setter
Step 0 of 10Ready
Your turn: follow the blue line

Reading fullName calls a getter. Assigning to fullName calls a setter with the assigned value.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  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);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Real-life analogyA setter is a receptionist for deliveries

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

VERIFY

Descriptors 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.

Data properties store a value; accessor properties run functions when the property is read or written.
QuestionData propertyAccessor property
Descriptor fieldsvalue, writable, enumerable, configurableget, set, enumerable, configurable
ReadingReturns the stored valueCalls get and returns what it returns
WritingStores a new value when writable is trueCalls set(value) when a setter exists
Best forPlain state such as firstNameComputed, validated, logged, or compatibility names
Inspect and define accessor descriptorsPop out in the code editor (opens in a new tab)JavaScript
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

INTERACTIVE

Computed 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.

Temperature and fullName playground
Computed and validated accessorsPop out in the code editor (opens in a new tab)JavaScript
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;  }};
Live objectaccepted
firstNameAda
lastNameLovelace
fullName getterAda Lovelace
_celsius backing data20°C
fahrenheit getter68°F
Try it yourself

The fullName getter now returns "Ada Lovelace". The fahrenheit getter returns 68 from 20°C.

Type names or temperatures. The visible inputs stand in for assignments: changing Celsius goes through the validating setter, while the dependent getters update from current data.

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.

Every getter read calls the function
Step 0 of 11Ready
Your turn: follow the blue line

Step through two property reads and one assignment. The getter runs on each read; the setter receives the assigned value.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  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);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Performance and side effects

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 CASES

If 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.

Getter-only assignmentJavaScript
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.

Recursive setter trapPop out in the code editor (opens in a new tab)JavaScript
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 that caches itself
Step 0 of 6Ready
Your turn: follow the blue line

A lazy getter can compute once, then redefine itself as an ordinary data property for future reads.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  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);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Compatibility wrappers

MIGRATION

Accessors 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.

Deprecation wrapperPop out in the code editor (opens in a new tab)JavaScript
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.fullName from 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.
Getter, setter, method, or plain property?
  • user.fullName assembled from firstName and lastName
  • Reject sensor.celsius = -500 with a RangeError
  • profile.save() sends data to storage
  • article.id is already stored and needs no logic
  • oldName forwards to newName during a migration
  • Trim and lowercase account.email = value
  • buildReport() performs expensive work on demand
  • createdAt stores the timestamp already known
Try it yourself
0 of 8 correct

Sort each design choice by the simplest API that tells the truth about the work being done.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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.” _name is 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 EXERCISES
Exercise 1 · Warm-upAdd a validated setter

Run the code, then try changing the assignment to an impossible temperature. What does the valid version print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
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);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPredict getter call counts

    Before running it, predict the final value of box.calls.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const box = {
      calls: 0,
      get label() {
        this.calls += 1;
        return "box";
      }
    };
    console.log(box.label);
    console.log(box.label + "!");
    console.log(box.calls);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFix a recursive setter

      The starter overflows the stack. Rewrite it so assigning person.name = " Ada " stores a trimmed name.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const person = {
        set name(value) {
          this.name = value;
        }
      };
      person.name = "Ada";

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWrite a lazy cached getter

        Adapt the pattern for an invoice total. What number appears as the cached descriptor value?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        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);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeAdd a deprecation wrapper

          Keep oldName working while the real property becomes newName. What new name prints after assignment?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          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);

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Getters & setters quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Which descriptor shape belongs to an accessor property?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this getter example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const user = {
                first: "Ada",
                last: "Lovelace",
                get name() { return this.first + " " + this.last; }
              };
              console.log(user.name);

              Choose an answer to see the explanation.

            3. Question 3 of 7Why should a setter usually write to _name instead of name inside set name(value)?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does this temperature setter print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const 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.

            5. Question 5 of 7A getter-only property is assigned in sloppy mode. What happens?

              Choose an answer to see the explanation.

            6. Question 6 of 7What is a good use for a compatibility wrapper?

              Choose an answer to see the explanation.

            7. Question 7 of 7What do these repeated getter reads print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const 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, not value/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.

            CompleteFrontend Clear concepts. Working examples.