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.
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.
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); // 100The 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.
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 fieldsPrivate 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.
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); // 10The 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.
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.
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); // 212The 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.
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.
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({})); // falseTrying 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
| Approach | Enforcement | Example |
|---|---|---|
| Underscore convention | None, an honor system | this._balance = 0 |
| Hash private fields | Enforced by the language | #balance = 0 |
| WeakMap pattern | Enforced at runtime | const 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.
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
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.
Frequently Asked Questions
Can I access a private field using bracket notation like obj['#field']?
Are private fields inherited by subclasses?
What happens if I try to access a private field on the wrong object?
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.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.