JavaScriptPrivate Fields

Private Fields

Until recently, JavaScript had no way to actually hide a property on an object — only conventions like a leading underscore. Private class fields, marked with a # prefix, fix that. They are truly inaccessible from outside the class body, even by reflection. Combined with classes and getters, they give you proper encapsulation.

The basic syntax

JS
class Counter {
  #count = 0;          // declared and initialised — private

  inc() { this.#count++; }
  get value() { return this.#count; }
}

const c = new Counter();
c.inc(); c.inc(); c.inc();
console.log(c.value);    // 3

try {
  console.log(c.#count);   // SyntaxError — outside the class
} catch (_) {}

Note that #count is part of the identifier — there is no separate "private" keyword. You declare #name once at the top of the class, and refer to it as this.#name everywhere inside.

How private it actually is

Truly private — not just a convention. Reading or writing a private field from outside the class is a syntax error, not a runtime error you might catch.

  • Object.keys, for...in, and spread do not see private fields.

  • JSON.stringify does not include them.

  • You cannot reach them with obj["#count"] — the brackets do not address private slots.

  • Subclasses cannot read parent private fields directly. They are private to the class, not the instance.

  • Engines often implement private fields as a separate WeakMap behind the scenes.

Private vs underscore convention

The pre-private convention was to prefix "private" properties with an underscore — _count, _init. It was advisory only. Anything could still read or modify them, and they showed up in iteration.

Old convention — soft privacy

JS
class Counter {
  constructor() { this._count = 0; }     // accessible from anywhere
  inc() { this._count++; }
}

const c = new Counter();
c._count = 9999;        // perfectly legal — no protection

Modern — hard privacy

JS
class Counter {
  #count = 0;
  inc() { this.#count++; }
}

const c = new Counter();
// c.#count = 9999;     // SyntaxError

Stick with # for new code. The underscore convention is still common in older codebases — read it as "treat as private", but understand it is not enforced.

Private methods

Methods and static methods can also be private. Use them to break a long public method into helpers without exposing the helpers.

JS
class Password {
  #value;

  constructor(plain) {
    this.#value = this.#hash(plain);
  }

  check(plain) {
    return this.#hash(plain) === this.#value;
  }

  // Pretend this is a real hash
  #hash(s) {
    return [...s].reduce((acc, ch) => acc + ch.charCodeAt(0), 0).toString(16);
  }
}

const pw = new Password("hunter2");
console.log(pw.check("hunter2"));    // true
console.log(pw.check("wrong"));      // false
Checking with the in operator

The expression #name in obj works inside a class body and returns whether obj has that private field. It is the idiomatic way to check whether an object is actually an instance of this class — including against forged copies.

JS
class Wallet {
  #balance = 0;

  static isWallet(value) {
    try {
      return #balance in value;
    } catch {
      return false;
    }
  }
}

const w = new Wallet();
console.log(Wallet.isWallet(w));         // true
console.log(Wallet.isWallet({}));        // false
Accessing on the wrong object throws
Reading `this.#balance` on an object that does not have that private slot throws a `TypeError`. That is the runtime check that backs the syntax-level privacy.
Private and inheritance

Private fields belong to the declaring class, not the chain. A subclass does not inherit them; if it declares #name of its own, that is a separate slot.

JS
class Base {
  #id = 1;
  getId() { return this.#id; }
}

class Sub extends Base {
  // this.#id   // SyntaxError — Base's private, not visible here
  getCustomId() { return this.getId(); }   // ok via inherited method
}

console.log(new Sub().getCustomId());   // 1
Static private members

JS
class Counter {
  static #total = 0;
  static increment() { return ++Counter.#total; }
  static get total() { return Counter.#total; }
}

Counter.increment();
Counter.increment();
console.log(Counter.total);   // 2
Why bother with hard privacy?
Real privacy protects internal invariants from accidental tampering during refactors, prevents external code from depending on undocumented state, and lets you safely change implementations. The underscore convention does none of those things.
One sentence
`#name` makes a field, method, or static truly private — invisible and inaccessible outside the class body. Use it whenever an implementation detail should stay an implementation detail.