Creating Private Class Fields in Modern JS

Private class fields keep data truly hidden inside a class. Learn the # syntax for private fields, private methods, private getters and setters, and how they differ from the old underscore convention.

6 min read

A private class field is a property that cannot be accessed from outside the class that declares it. Before private fields existed, developers used an underscore prefix like _secret to signal that a property was internal, but that property was still fully readable and writable from anywhere. The hash syntax makes privacy real: JavaScript itself enforces it.

javascriptjavascript
class BankAccount {
  #balance = 0;
  deposit(amount) {
    this.#balance += amount;
    return this.#balance;
  }
  get balance() {
    return this.#balance;
  }
}
 
const account = new BankAccount();
account.deposit(100);
console.log(account.balance); // 100

The balance field is only reachable from inside the class body that declares it. Any attempt to read or write it from outside throws a syntax error at parse time, before the code even runs.

The Hash Syntax

A private identifier starts with a hash followed by a name. The hash is part of the name itself, not a string prefix, so you cannot build a private name dynamically.

javascriptjavascript
class Example {
  #data = "secret";
 
  log() {
    console.log(this.#data); // OK, called from inside the class
  }
}
 
const ex = new Example();
ex.log();
// ex.#data;     throws a syntax error
// ex["#data"];  undefined, bracket notation cannot find private fields

Private fields must be declared in the class body before they are used. Unlike normal properties, you cannot attach one to an instance after it is created.

Private Instance Fields

Private instance fields are the most common form. Each instance gets its own copy, and no outside code can touch it directly.

javascriptjavascript
class Player {
  #score = 0;
  scorePoint(points) {
    this.#score += points;
  }
  get score() {
    return this.#score;
  }
}
 
const p1 = new Player();
p1.scorePoint(10);
console.log(p1.score); // 10

The outside world only sees the public scorePoint method and score getter. The underlying score data stays fully protected from accidental mutation elsewhere in the codebase. The same pattern applies to any number of private fields on a class, such as lives, inventory, or internal counters.

Private Methods

Methods can be private too. Prefixing a method name with a hash makes it callable only from inside the class.

javascriptjavascript
class Validator {
  #isPositiveInteger(value) {
    return Number.isInteger(value) && value > 0;
  }
  validateAge(age) {
    if (!this.#isPositiveInteger(age)) {
      return "Age must be a positive whole number";
    }
    return "Valid age";
  }
}
 
const v = new Validator();
console.log(v.validateAge(25)); // "Valid age"

Private methods let you split internal logic into small helpers without exposing them as part of the class API. This keeps the public surface small and stops callers from depending on implementation details that might change later.

Private Getters and Setters

Private getters and setters work the same way as public ones, just written with a leading hash on the name.

javascriptjavascript
class Temperature {
  #celsius = 0;
  set #validated(value) {
    if (typeof value !== "number") throw new TypeError("must be a number");
    this.#celsius = value;
  }
  constructor(celsius) {
    this.#validated = celsius;
  }
  get fahrenheit() { return this.#celsius * 9 / 5 + 32; }
}
 
const t = new Temperature(100);
console.log(t.fahrenheit); // 212

The private setter validates the value before storing it. That validation logic stays invisible to anyone using the class; they only see a normal constructor and a public fahrenheit getter. A matching private getter would read #validated the same way a public getter reads a normal field.

Private Static Fields and Methods

A private field or method can also be static. It belongs to the class rather than to instances, and it is still only reachable from inside the class body.

javascriptjavascript
class ConnectionPool {
  static #max = 10;
  static #active = 0;
 
  static connect() {
    if (this.#active >= this.#max) {
      throw new Error("Connection pool exhausted");
    }
    this.#active += 1;
    return `Connected (${this.#active}/${this.#max})`;
  }
}
 
console.log(ConnectionPool.connect()); // "Connected (1/10)"

Private static members work well for internal counters, caches, or configuration that should never leak into the public API. One caveat matters for inheritance: always reference private static fields through the class name, not through this, inside a static method. Calling that method on a subclass replaces this with the subclass, and the subclass does not share the parent's private names, which throws an error.

Checking If a Private Field Exists

The in operator works with private fields to check whether a given object has one, which is the only safe way to test from a static method.

javascriptjavascript
class Person {
  #id;
  constructor(id) {
    this.#id = id;
  }
  static hasId(obj) {
    return #id in obj;
  }
}
 
const p = new Person(123);
console.log(Person.hasId(p));  // true
console.log(Person.hasId({})); // false

Trying to access obj.#id directly on an object that was not created by the class throws a TypeError, so the in check is the safer way to test for a private field's presence.

Private Fields vs the Underscore Convention

ApproachEnforcementExample
Underscore conventionNone, an honor systemthis._balance = 0
Hash private fieldsEnforced by the language#balance = 0
WeakMap patternEnforced at runtimeconst data = new WeakMap()

The underscore convention is not privacy. It is a signal that anyone can ignore. Private fields are enforced at the syntax level: the parser rejects direct access from outside the class, which catches mistakes before the code ever runs. The WeakMap pattern from older code gives runtime privacy but is verbose to set up, so for new code the hash syntax is the standard choice.

Common Mistake: Private Fields Are Class-Scoped

Private fields belong to the class body that declares them, not to a specific instance's inheritance chain. A subclass cannot see its parent's private fields.

javascriptjavascript
class Animal {
  #name = "unknown";
  getName() { return this.#name; }
}
 
class Dog extends Animal {
  bark() { return `${this.getName()} barks!`; }
}
 
const dog = new Dog();
console.log(dog.bark()); // "unknown barks!"

Dog cannot access the parent's private name field directly and must go through the public getName method that Animal provides. This is by design: it keeps each class's internals encapsulated, even from its own subclasses.

For more on how classes, inheritance, and prototypes work together, see class inheritance and the prototype chain.

Rune AI

Rune AI

Key Insights

  • Prefix a field or method name with # to make it private to the class.
  • Private fields cannot be read, written, or deleted from outside the class body.
  • Private methods, getters, and setters follow the same # syntax.
  • Private static fields belong to the class itself and are not visible on instances.
  • Use #field in obj to safely check whether an object has a private field.
RunePowered by Rune AI

Frequently Asked Questions

Can I access a private field using bracket notation like obj['#field']?

No. The # is part of the identifier name, not a string character. Private fields can only be accessed with dot notation (obj.#field) inside the class body. Bracket notation and dynamic access do not work.

Are private fields inherited by subclasses?

No. Private fields are scoped to the class that declares them. A subclass cannot directly access a parent's private field. If a subclass needs to read or change that data, the parent class must expose a public or protected-style method.

What happens if I try to access a private field on the wrong object?

You get a TypeError. Unlike normal properties, which return undefined when missing, private fields throw an error if the object does not have them. You can check with #field in obj before accessing.

Conclusion

Private class fields bring real encapsulation to JavaScript. Use the # prefix for fields and methods that should not be touched from outside the class. Prefer private over the old underscore convention. Remember that private means private to the class, not to the instance, and that subclasses cannot see their parent's private members.