Getters, Setters & Private Fields

Getters and setters let you control how a property is read or written. Private fields hide internal data so the outside world can't accidentally break it.

What Are Getters and Setters?

A getter is a special method that runs when you read a property. A setter is a special method that runs when you write to a property. From the outside, they look just like regular properties β€” but behind the scenes, code runs automatically.

🌑️ Real-life analogy

Think of a thermostat. You can read the temperature (getter) and set a new temperature (setter). But when you "set" it, the thermostat runs its own logic β€” checking limits, turning on/off the heater, etc. You just see a number; the smart behavior is hidden inside.

Getters β€” get

Use the get keyword to define a getter. It looks like a method but you access it like a property (no parentheses).

class Circle {
  constructor(radius) {
    this.radius = radius;
  }

  get area() {
    return Math.PI * this.radius ** 2;
  }

  get diameter() {
    return this.radius * 2;
  }

  get circumference() {
    return 2 * Math.PI * this.radius;
  }
}

const c = new Circle(5);

c.area;          // 78.54… β€” reads like a property, runs like a method
c.diameter;      // 10
c.circumference; // 31.41…

// No parentheses! c.area() would throw an error β€” it's a getter, not a method
  • get area() { ... } β€” defines a getter named area
  • c.area β€” reading this automatically calls the getter function
  • The getter returns a computed value β€” you don't store it separately
  • No parentheses when accessing: c.area not c.area()
πŸ’‘ When to use a getter

Use a getter when a property is derived from other data β€” like area from radius, or fullName from firstName and lastName. Instead of storing and manually updating it, compute it fresh each time it's read.

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const p = new Person("Alice", "Smith");
p.fullName; // "Alice Smith" β€” no need to store it separately

p.firstName = "Bob";
p.fullName; // "Bob Smith"  β€” auto-updates because it's computed

Setters β€” set

Use the set keyword to define a setter. It runs when you assign a value to that property. Setters are perfect for validating values before they're stored.

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;  // this calls the setter below
  }

  get age() {
    return this._age;
  }

  set age(value) {
    if (typeof value !== "number" || value < 0) {
      throw new Error("Age must be a positive number");
    }
    this._age = value;
  }
}

const p = new Person("Alice", 30);
p.age;      // 30 β€” calls the getter
p.age = 31; // calls the setter, validates, stores 31
p.age = -5; // throws Error: "Age must be a positive number"
  • set age(value) { ... } β€” runs when you write p.age = something
  • this._age β€” we store the actual value in _age (underscore convention means "internal")
  • If we stored in this.age inside the setter, it would call the setter again β€” infinite loop!
  • The getter get age() reads back from this._age
  • From outside, you just write p.age = 31 β€” clean and simple, validation is hidden
πŸ“Œ Getter-only properties (read-only)

If you define a get but no matching set, the property is read-only. Trying to assign to it in strict mode throws an error; in non-strict mode the assignment is silently ignored.

class Constant {
  get PI() { return 3.14159; }
}
const c = new Constant();
c.PI = 99; // silently ignored (or throws in strict mode)
c.PI;      // still 3.14159

Private Fields β€” #

A private field starts with a # and can only be accessed inside the class. Nothing outside β€” not even a subclass β€” can read or write it directly. This is called encapsulation: hiding internal details so they can't be accidentally broken from the outside.

class BankAccount {
  #balance = 0;       // private β€” no one outside can touch this directly
  #owner;             // private β€” declared here, set in constructor

  constructor(owner, initialBalance) {
    this.#owner = owner;
    this.#balance = initialBalance;
  }

  deposit(amount) {
    if (amount <= 0) throw new Error("Amount must be positive");
    this.#balance += amount;
  }

  withdraw(amount) {
    if (amount > this.#balance) throw new Error("Insufficient funds");
    this.#balance -= amount;
  }

  get balance() {
    return this.#balance; // read-only access via getter
  }

  get owner() {
    return this.#owner;
  }
}

const account = new BankAccount("Alice", 100);

account.deposit(50);
account.balance;   // 150 β€” via getter
account.withdraw(30);
account.balance;   // 120

account.#balance;  // SyntaxError β€” cannot access private field from outside
account.#balance = 999999; // SyntaxError β€” cannot write it either
  • #balance = 0 β€” declares a private field with a default value of 0
  • #owner β€” declares a private field (no default; set in constructor)
  • this.#balance += amount β€” accessing private fields inside the class is fine
  • get balance() β€” exposes the private field as read-only from outside
  • Trying to do account.#balance outside the class is a SyntaxError β€” the JS engine rejects it entirely
πŸ’‘ Private fields vs underscore convention

Before private fields existed, developers used an underscore (_balance) to signal "don't touch this." But it was just a social convention β€” nothing stopped someone from writing account._balance = 999. Private fields (#balance) are enforced by the JavaScript engine itself β€” any access from outside throws a SyntaxError.

_underscore#privateField
Truly private?No β€” just a hintYes β€” enforced by JS
Access from outside?Still possibleSyntaxError
When added?Old conventionES2022

Private Methods β€” #method()

Methods can be private too. Use # before the method name. Private methods can only be called from inside the class.

class Form {
  #data = {};

  constructor(data) {
    this.#data = data;
  }

  // Private method β€” internal helper, not part of the public API
  #validate() {
    if (!this.#data.email) {
      throw new Error("Email is required");
    }
    if (!this.#data.name) {
      throw new Error("Name is required");
    }
  }

  submit() {
    this.#validate(); // only accessible inside the class
    console.log("Form submitted!", this.#data);
  }
}

const form = new Form({ name: "Alice", email: "alice@example.com" });
form.submit();   // works β€” submit() calls #validate() internally
form.#validate(); // SyntaxError β€” can't call private method from outside

Private Static Fields and Methods

You can combine #private with static:

class IdGenerator {
  static #nextId = 1; // private AND static

  static generate() {
    return IdGenerator.#nextId++;
  }
}

IdGenerator.generate(); // 1
IdGenerator.generate(); // 2
IdGenerator.generate(); // 3

IdGenerator.#nextId; // SyntaxError β€” private even from outside the class

Checking if a Private Field Exists β€” in operator

Since accessing obj.#field on an object that doesn't have that field throws an error (not just undefined), you can use the in operator to safely check:

class Dog {
  #name;
  constructor(name) { this.#name = name; }

  static isDog(obj) {
    return #name in obj; // true if obj has the private #name field
  }
}

Dog.isDog(new Dog("Rex")); // true
Dog.isDog({ name: "Rex" });  // false β€” plain object, no #name

Putting It All Together

Here's a complete example using private fields, getters, setters, and a private method together:

class Temperature {
  #celsius; // private storage

  constructor(celsius) {
    this.celsius = celsius; // calls the setter
  }

  // Private helper β€” validates the value
  #checkValid(val) {
    if (val < -273.15) {
      throw new RangeError("Below absolute zero!");
    }
  }

  get celsius() {
    return this.#celsius;
  }

  set celsius(val) {
    this.#checkValid(val);  // validate first
    this.#celsius = val;
  }

  // Read-only computed property
  get fahrenheit() {
    return this.#celsius * 9/5 + 32;
  }

  get kelvin() {
    return this.#celsius + 273.15;
  }
}

const t = new Temperature(100);
t.celsius;    // 100
t.fahrenheit; // 212
t.kelvin;     // 373.15

t.celsius = 0;
t.fahrenheit; // 32

t.celsius = -300; // RangeError: Below absolute zero!
t.#celsius;       // SyntaxError β€” private!

Common Mistakes

⚠ Watch out for
  • Storing in the same name as the getter/setter: If your getter is get age(), never store in this.age inside the setter β€” it calls the setter again (infinite loop). Use a private field: this.#age.
  • Calling a getter with parentheses: c.area() throws TypeError: c.area is not a function. It's a getter β€” use c.area without parentheses.
  • Declaring private fields only in the constructor: Private fields must be declared in the class body first (#field; or #field = value;), not just assigned in the constructor.
  • Using private fields in a subclass: A private field defined in the parent is truly private β€” the child class cannot access it. This is intentional.
class Parent {
  #secret = 42;
}
class Child extends Parent {
  read() {
    return this.#secret; // SyntaxError β€” #secret belongs to Parent only
  }
}

Best Practices

  • Use private fields (#field) for any data that should not be changed directly from outside the class.
  • Use a getter to expose computed or derived values as if they were regular properties.
  • Use a setter to validate or transform data before storing it.
  • When you have both a getter and setter for the same name, always store the real data in a private field (#_name) to avoid infinite loops.
  • Prefer #privateField over the old _underscoreConvention β€” it's actually enforced by the language.

Performance Notes

Getters run a function every time the property is read β€” if the calculation is expensive and you read it often, consider caching the result. Private fields are just as fast as regular properties in modern engines; the privacy check is done at parse time, not at runtime.

Browser Compatibility

Getters and setters (get/set) have been in JavaScript since ES5 and work everywhere. Private fields (#field) were added in ES2022 and are supported in all modern browsers (Chrome 74+, Firefox 90+, Safari 14.1+, Edge 79+). Private methods are ES2022 as well. For older environments, use the _underscore convention as a fallback.

Interview Questions

What is the difference between a getter and a regular method?

Both run code when you access them, but a getter is accessed like a property (no parentheses: obj.name), while a method is called with parentheses (obj.getName()). Getters make the API cleaner when you want something that feels like a property but computes its value dynamically.

Why do you need a private backing field when using getters and setters?

If your getter is named age and you try to store this.age = value inside the setter, it calls the setter again β€” creating an infinite loop. Storing in a separate private field (this.#age) avoids this: reading this.#age in the getter doesn't trigger the getter again.

What makes private fields (#) better than the underscore convention (_)?

The underscore is just a naming convention β€” anyone can still access or modify obj._privateField from outside. Private fields with # are enforced by the JavaScript engine itself: accessing obj.#field from outside the class is a SyntaxError, making the data truly inaccessible.

Can a subclass access a parent's private fields?

No. Private fields are truly private to the class they are defined in. Even child classes cannot access them. If a subclass needs to work with that data, the parent must provide a getter, setter, or method.

✏️ Exercise

Create a Rectangle class with private fields #width and #height. Add getters for both, setters that reject non-positive values, and a read-only getter area that returns width Γ— height.

🧠 Quiz

1. How do you read a getter? With or without parentheses?

Without parentheses: obj.myGetter, not obj.myGetter().

2. What happens if you try to access a private field from outside the class?

JavaScript throws a SyntaxError β€” it's not even allowed to compile, unlike a regular property access that would just return undefined.

3. Where must you declare a private field?

At the top of the class body, before the constructor β€” either as #field; or #field = defaultValue;. You cannot create a private field only inside a constructor without declaring it first.

πŸš€ Mini Challenge

Build a Password class that stores a password in a private field. Add a setter that throws an error if the password is shorter than 8 characters. Add a getter that returns "β€’β€’β€’β€’β€’β€’β€’β€’" (always hidden). Add a method verify(attempt) that returns true if the attempt matches the real password.

Summary

  • Getters (get prop()) run a function when you read a property β€” accessed without parentheses.
  • Setters (set prop(value)) run a function when you assign to a property β€” great for validation.
  • Always store the value in a private backing field (#field) when using getters/setters to avoid infinite loops.
  • Private fields (#field) are truly inaccessible outside the class β€” enforced by the JS engine, not just convention.
  • Private methods (#method()) work the same way β€” internal helpers the outside world can't call.
  • Combine all three to build classes with controlled, safe interfaces.

Related Topics

MDN reference: developer.mozilla.org β†’ JavaScript β†’ Classes β†’ Private class features / get / set