Skip to main content

Private class fields outside a class

You cannot reach a private class property from outside: obj.#field outside the class body is invalid syntax, and the engine throws a SyntaxError before execution even starts. This is not a convention like the _ prefix, it is real language level privacy (ES2022) that cannot be bypassed through a string key, through Reflect, or through serialisation.

Theory

TL;DR

  • obj.#field outside the class is a SyntaxError at parse time, not a runtime error.
  • obj['#field'] is not an access to the private field: it returns undefined or creates an ordinary public property named '#field'.
  • Private fields never appear in Object.keys, Object.entries, Object.getOwnPropertyNames, Reflect.ownKeys or for...in.
  • JSON.stringify ignores private fields: an object that only has them serialises to {}.
  • The only legal access is through public methods, getters and setters declared in that same class.

Quick example

javascript
class User { #password = 'secret-1'; checkPassword(pwd) { return pwd === this.#password; } } const u = new User(); console.log(u.checkPassword('secret-1')); // true, access through a class method // console.log(u.#password); // SyntaxError: Private field '#password' // // must be declared in an enclosing class

The u.#password line will not even run: its mere presence in the file makes the whole module fail to parse.

Why it is a parse error, not a runtime error

Privacy in JavaScript is implemented at the syntax level, not through a naming convention. When the engine sees the #field token, it performs two checks at compile time:

  1. Is this private name declared in the lexical environment of the class that encloses the expression?
  2. If not, it throws a SyntaxError and does not even attempt to execute the code.

That is why such an error cannot be caught with a try/catch around the access: by the time try could run, the script has already failed to compile.

javascript
class Example { #value = 42; } const e = new Example(); try { // console.log(e.#value); // uncommenting breaks the WHOLE file, not just this block } catch (err) { console.log('we never get here'); }

Workarounds do not work

Private fields are not ordinary object properties, so no enumeration technique reveals them.

javascript
class User { #password = 'secret-1'; } const u = new User(); console.log(u['#password']); // undefined console.log(Object.keys(u)); // [] console.log(Object.entries(u)); // [] console.log(Object.getOwnPropertyNames(u)); // [] console.log(Reflect.ownKeys(u)); // [] for (const key in u) { console.log(key); // never iterates }

Serialisation does not help either: private fields do not reach JSON.

javascript
class Account { #balance = 1000; } const acc = new Account(); console.log(JSON.stringify(acc)); // '{}'

This is a handy property: private fields do not leak into logs or an HTTP response body by accident, because they are simply absent from the JSON.stringify result.

A private field cannot be shadowed

Writing to a private field from outside is a syntax error as well. Writing through a string key creates a completely different, public property that has nothing to do with the private one.

javascript
class Example { #value = 42; getValue() { return this.#value; } } const e = new Example(); // e.#value = 100; // SyntaxError e['#value'] = 100; // creates a new ordinary property console.log(e); // Example { '#value': 100 } console.log(e.getValue()); // 42, the real private field did not change

In other words, from outside you can only pollute the object with a similar looking key, while the real class state stays untouched.

The only correct access: the public interface

If a private value has to be visible outside, the class itself decides in what shape to expose it. That is what methods, getters and setters are for: they can touch #field because they are declared in the same class.

javascript
class User { #password = 'secret-1'; get maskedPassword() { return '*'.repeat(this.#password.length); } checkPassword(pwd) { return pwd === this.#password; } setPassword(newPwd) { if (newPwd.length < 5) { throw new Error('Password is too short'); } this.#password = newPwd; } } const u = new User(); console.log(u.maskedPassword); // '********' console.log(u.checkPassword('secret-1')); // true u.setPassword('secret-2');

The field stays protected, but the class offers controlled, safe access: masking on read and validation on write.

Checking whether a private field exists

The only #field operation allowed outside of a this access is the #field in obj check, and even that works only inside the class body. It is used as a brand check, to tell whether an arbitrary object is really an instance of this class.

javascript
class User { #password = 'secret-1'; static isUser(obj) { return #password in obj; // true only for User instances } } console.log(User.isUser(new User())); // true console.log(User.isUser({})); // false

Outside the class that expression is a SyntaxError again, so it is not a way to read the value from outside, it is a way to check a type safely from inside.

Summary table

Action from outside the classResult
obj.#fieldSyntaxError at parse time
obj.#field = 1SyntaxError at parse time
obj['#field']undefined, or creates a separate public property
Object.keys(obj)private field not visible
Object.getOwnPropertyNames(obj)private field not visible
Reflect.ownKeys(obj)private field not visible
for...inprivate field not visible
JSON.stringify(obj)private field not visible
a class method, getter or setterworks, this is the only way

Common mistakes

  • Confusing #field with the _field convention. A _password property is an ordinary public field: it shows up in Object.keys and anyone can overwrite it. #password is physically unreachable.
  • Assuming obj['#field'] is the same field. It is a separate string key; writing through it creates a new public slot and does not change the private state.
  • Trying to catch the error with try/catch. A SyntaxError happens before execution, so the whole file or module fails, not one block.
  • Expecting private fields in JSON. They disappear after JSON.stringify, so a DTO has to be built explicitly, for example with a toJSON() method.
  • Declaring a private field only in the constructor. The private name must be declared in the class body (#password = ... or #password;), otherwise assigning this.#password is a syntax error too.
  • Expecting private fields to be inherited. A subclass does not see the parent #field: private names belong to one specific class body, not to the prototype chain.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.