Private fields & methods
Learn JavaScript private fields, private methods, private static members, brand checks, and when to choose class privacy instead of closures or WeakMaps.
- 01Lock class stateUse
#privatefields, methods, and getters for real language-enforced encapsulation. - 02Check brands safelyUse
#x in objto ask whether an object was built by a class. - 03Choose a privacy patternCompare private fields with closures and WeakMap privacy.
Real privacy for class objects
JavaScript objects are usually open. If an object has a property named balance, other code can read it, write it, delete it, or accidentally overwrite it. Naming it _balance is only a convention: a polite sign that says “please don’t touch.”
A private class member starts with #, such as #balance or #validate(). The # is not part of a string property name. It creates a private name that only code inside the declaring class body can use. Outside code cannot even parse account.#balance.
Imagine every account object carries a safe. The public methods—deposit, withdraw, and balance label—know the combination. Other code can hold the object, pass it around, and ask it to do work, but it cannot open the safe directly.
- In real life: The safe inside the bank account folder
- In JavaScript: The private field
#balanceon the object - In real life: Staff who know the combination
- In JavaScript: Methods declared inside the class
- In real life: A sticky note reading “do not touch”
- In JavaScript: An
_balancenaming convention - In real life: Asking whether this folder came from the bank
- In JavaScript: A brand check:
#balance in obj
Where the analogy stops: A real safe can be drilled open. JavaScript private names are enforced by the parser and runtime, but debugging tools may still display them to the developer who owns the process. Privacy is for program correctness, not secret storage against the machine owner.
Private members are class fields, methods, accessors, or static members whose names start with # and are usable only inside the class body that declares them.
This lesson connects the privacy patterns you met in Closures, Closure patterns, and WeakMap & WeakSet to the class syntax from Class basics.
#private fields & methods
INTERACTIVEPrivate instance fields are declared in the class body, then each new instance receives its own private slot. Methods in that same class can read and write the slot with this.#name. Private methods and private getters follow the same access rule.
class BankAccount { #balance = 100; deposit(amount) { this.#validate(amount); this.#balance += amount; return this.balanceLabel; } withdraw(amount) { this.#validate(amount); this.#balance -= amount; return this.balanceLabel; } get balanceLabel() { return "₹" + this.#balance; } #validate(amount) { if (amount < 0) throw new RangeError("No negative amounts"); }} const account = new BankAccount();// account.#balance is a SyntaxError outside the class.account["#balance"] = 999; // a normal public propertyThe safe is inside the object. The buttons are class methods that know the combination; outside code can only tape a separate label named #balance onto the outside.
Start at ₹100. Use the methods; the private field can only change through class code.
Try the buttons. The public methods change the real private balance. The Try account.#balance button shows the SyntaxError that our tests prove with a parser. The fake bracket property demonstrates the key distinction: account["#balance"] is just a normal public property whose string key happens to contain a hash mark.
Predict the printed balance. Then step through how public methods reach private state.
script
class BankAccount { #balance = 0; constructor(opening) { this.#validate(opening); this.#balance = opening; } get balanceLabel() { return "₹" + this.#balance; } deposit(amount) { this.#validate(amount); this.#balance += amount; return this.balanceLabel; } #validate(amount) { if (amount < 0) throw new RangeError("Amount must be positive"); }} account.deposit(25);console.log(account.balanceLabel);Code like account.#balance outside the class is not a failed property lookup. It is invalid syntax. Duplicate private names in the same class are invalid syntax too. By contrast, a method that tries to read its private field from the wrong object parses successfully, then throws a TypeError when it runs.
class Account { #balance = 100; }const account = new Account();account.#balance;class Account { #balance = 100; read(other) { return other.#balance; }}new Account().read({});- Inside
deposit():this.#balance += amount - Inside
withdraw():this.#validate(amount) - Outside the class:
account.#balance - In a subclass method:
this.#balance - Outside:
account["#balance"] JSON.stringify(account)
Sort each code location. The answer turns on whether the code is inside the declaring class body, outside it, or only touching a public look-alike property.
Private static members
CLASS STATEstatic #count belongs to the class itself, not to each instance. Use private static fields for class-wide counters, registries, caches, and helper methods that should not be part of the public API.
class Vault { static #count = 0; static #lastId = 0; #id; constructor() { Vault.#count += 1; this.#id = ++Vault.#lastId; } static get count() { return Vault.#count; } get label() { return "vault-" + this.#id; }} const first = new Vault();const second = new Vault();console.log(Vault.count);console.log(first.label + ", " + second.label);The two vault instances have separate private instance IDs, but they share one private static count. Public code can ask Vault.count; it cannot read Vault.#count.
Static methods are inherited, and inside a static method this is the class used for the call. That is useful for public statics, but private static fields are branded to the class that declared them. If an inherited static method uses this.#count and a subclass calls it, V8 throws TypeError: Cannot read private member #count from an object whose class did not declare it. Name the declaring class, such as BaseVault.#count, when that is what you mean.
class BaseVault { static #count = 1; static readWithThis() { return this.#count; } static readWithName() { return BaseVault.#count; }}class ChildVault extends BaseVault {}console.log(BaseVault.readWithThis());try { ChildVault.readWithThis();} catch (error) { console.log(error.name + ": " + error.message);}console.log(ChildVault.readWithName());#x in obj brand checks
STEP THROUGHSometimes you want to know whether a value is one of your class’s instances without reading private data. Inside the class, #balance in value asks “does this object carry the private brand installed by this class?” It returns a boolean and does not open the safe.
Brand checks ask whether this object carries the private brand; they do not read the private value.
script
class BankAccount { #balance = 0; static isAccount(value) { return typeof value === "object" && value !== null && #balance in value; }} const fake = { "#balance": 999 };console.log(BankAccount.isAccount(account));console.log(BankAccount.isAccount(fake));fake["#balance"] = 123;console.log(BankAccount.isAccount(fake));console.log(fake["#balance"]);A bank clerk can ask, “is this one of our folders?” by checking the stamp on the folder. They do not need to open the safe or count the money.
- In real life: A stamped bank folder
- In JavaScript: An object with the class’s private brand
- In real life: A fake folder with a note saying balance
- In JavaScript: A plain object with
"#balance"as a public key - In real life: Checking the stamp without opening the safe
- In JavaScript:
#balance in objreturning true or false
Where the analogy stops: The brand check only proves that this private field was installed. It does not prove business validity, freshness, permissions, or that public properties were not changed.
Private fields also stay out of ordinary reflection. They do not appear in Object.keys, object spread, or JSON.stringify. But if you add a public property named "#balance", that public property is enumerable like any other.
class Account { #balance = 100; owner = "Ada"; label() { return "₹" + this.#balance; }}const account = new Account();console.log(Object.keys(account).join(","));console.log(JSON.stringify(account));account["#balance"] = 999;console.log(Object.keys(account).join(","));console.log(JSON.stringify(account));Private fields vs closures
COMPAREYou already know three ways to hide state. Closures hide variables in a function’s remembered scope. WeakMaps hide data in a side table keyed by objects. Private fields hide state in class instances with syntax the language enforces.
| Question | Private fields | Closures | WeakMap privacy |
|---|---|---|---|
| Privacy strength | Language enforced; outside obj.#x is a syntax error | Private as long as you never expose the closed-over variable | Private as long as the WeakMap stays hidden |
| Memory shape | State lives on each instance in private slots | Each factory call keeps an environment alive | State lives in a side table keyed by objects |
| Inheritance | Subclasses cannot access the parent’s private names directly | No class inheritance unless you build it | Methods can share the module-level WeakMap if you design it that way |
| Debugging visibility | DevTools may show private slots, but code still cannot read them | Usually appears as closure variables | Usually appears as a WeakMap entry or hidden side table |
| Object.keys / JSON | Skipped | Not object properties | Skipped because data is outside the object |
Prefer private fields when you are already designing a class and each instance needs its own protected state. Prefer closures for small factories, function modules, and one-off remembered values. Prefer WeakMaps when you need to attach hidden data to objects you do not control, or when a module-level side table is clearer than changing a class.
Where you’ll use this
Private members are not only for bank accounts. They are useful whenever you need to protect invariants: rules that must stay true for an object to work.
- UI widgets: keep internal DOM references private and expose methods like
open()orclose(). - Data models: validate writes through methods so impossible states cannot be assigned directly.
- SDK clients: hide retry counters, caches, and normalized configuration.
- Factories: combine private static caches with public factory methods.
Do not store passwords, API secrets, or security decisions in browser JavaScript and assume #private protects them from the person running the browser. Private fields protect your program’s object boundaries, not secrets from the machine owner.
Common misconceptions
“An underscore makes a property private.”
No. _balance is a public property with a warning label. #balance is a language-enforced private name.
“Private fields are just strings with # in the name.”
No. account["#balance"] reads a public string key. It cannot touch #balance.
“Subclasses can read parent private fields.”
No. A subclass inherits public and protected-by-convention methods, but it is a different class body and cannot spell the parent’s private names.
“Brand checks read private data.”
No. #x in obj only returns whether the object has that private brand.
“Private fields are included in JSON.”
No. JSON and Object.keys see public enumerable properties only.
Practice: lock the right doors
5 EXERCISESRewrite a Timer that used _seconds so it uses #seconds. What should the program print?
class Timer {
#seconds = 0;
tick() {
this.#seconds += 1;
return this.#seconds;
}
}
const timer = new Timer();
console.log(timer.tick());
console.log(Object.keys(timer).length);class Timer {
#seconds = 0;
tick() {
this.#seconds += 1;
return this.#seconds;
}
}
const timer = new Timer();
console.log(timer.tick());
console.log(Object.keys(timer).length);The field is private, so tick() can update it, but Object.keys(timer) finds no public data properties.
Predict whether this prints a number, returns undefined, or errors.
class Account { #balance = 100; }
const account = new Account();
account.#balance;It is a SyntaxError. Outside the declaring class body, account.#balance is invalid syntax.
Implement Member.isMember(value) so it recognizes real instances without reading the private value.
class Member {
#id = 1;
static isMember(value) {
return typeof value === "object" && value !== null && #id in value;
}
}
console.log(Member.isMember(new Member()));
console.log(Member.isMember({ "#id": 1 }));class Member {
#id = 1;
static isMember(value) {
return typeof value === "object" && value !== null && #id in value;
}
}
console.log(Member.isMember(new Member()));
console.log(Member.isMember({ "#id": 1 }));The real instance has the private brand. The plain object has a public string key only, so the second line prints false.
Add a private method that rejects empty cart item names.
class Cart {
#items = [];
add(name) {
this.#requireName(name);
this.#items.push(name);
return this.#items.length;
}
#requireName(name) {
if (name === "") throw new RangeError("Name required");
}
}
const cart = new Cart();
console.log(cart.add("Tea"));class Cart {
#items = [];
add(name) {
this.#requireName(name);
this.#items.push(name);
return this.#items.length;
}
#requireName(name) {
if (name === "") throw new RangeError("Name required");
}
}
const cart = new Cart();
console.log(cart.add("Tea"));The public method delegates validation to a private helper, then updates the private array.
You are building a VideoPlayer class with play(), pause(), and an internal current time that must only change through validation. Which privacy pattern best fits?
Use private fields. Closures are great for factories, but class instances with methods, private helpers, and brand checks are exactly what #private was designed for.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7What does
#balancemean in a class?Choose an answer to see the explanation.
Question 2 of 7What does this account program print?
Read the code, then predictclass Account { #balance = 100; deposit(amount) { this.#balance += amount; return this.#balance; } } const account = new Account(); console.log(account.deposit(25));Choose an answer to see the explanation.
Question 3 of 7What happens if code outside the class writes
account.#balance?Choose an answer to see the explanation.
Question 4 of 7What do the brand-check lines print?
Read the code, then predictclass Account { #balance = 0; static isAccount(value) { return typeof value === "object" && value !== null && #balance in value; } } console.log(Account.isAccount(new Account())); console.log(Account.isAccount({ "#balance": 0 }));Choose an answer to see the explanation.
Question 5 of 7What do Object.keys and JSON print?
Read the code, then predictclass Account { #balance = 100; owner = "Ada"; } const account = new Account(); console.log(Object.keys(account).join(",")); console.log(JSON.stringify(account));Choose an answer to see the explanation.
Question 6 of 7Which static private access is safest inside a base class static method?
Choose an answer to see the explanation.
Question 7 of 7When are closures a better privacy fit than private fields?
Choose an answer to see the explanation.
Key takeaways
#privatenames are real private class members, not naming conventions.- Only the declaring class body can use a private field, method, getter, setter, or static member.
- Outside
account.#xand duplicate private declarations are SyntaxErrors; wrong-object private reads are runtime TypeErrors. #x in objchecks the private brand without reading the private value.- Private fields, closures, and WeakMaps are all useful privacy tools; choose by code shape.
Remember the one-liner.
A #private member is a real language lock: class methods know the combination; outside code does not.
Up next: Extending built-ins & instanceof.