
JavaScript’s private fields are the cleanest way to keep data truly private within a class. Unlike the old convention of prefixing properties with underscores to signal privacy, private fields are enforced by the language itself. This means they cannot be accessed or modified outside the class body, not even by subclasses or by any external code.
Private fields are declared by prefixing the field name with a hash # symbol right inside the class. This signals to the JavaScript engine that this property is private and should not be accessible outside the class scope.
class User {
#password;
constructor(username, password) {
this.username = username;
this.#password = password; // private field
}
checkPassword(input) {
return input === this.#password;
}
}
Notice how #password is only accessible inside the class methods. Any attempt to access user.#password from outside will throw a syntax error, not just return undefined. That’s a strict privacy guarantee, unlike the old weak conventions.
You can also initialize private fields right when declaring them:
class Counter {
#count = 0;
increment() {
this.#count++;
}
get value() {
return this.#count;
}
}
Here, #count starts at zero for every new instance, and there is no way to tamper with it externally—only the methods inside the class have that power.
Another subtlety worth noting is that private fields are specific to each class. If you create subclasses, private fields declared in the parent class are not accessible directly by the subclass. They are truly tied to the lexical scope of the class where they are declared.
class Base {
#secret = 42;
reveal() {
return this.#secret;
}
}
class Derived extends Base {
tryAccess() {
// This will throw a SyntaxError:
// return this.#secret;
}
}
In this example, Derived cannot access #secret directly, even though it inherits from Base. This enforces encapsulation at the language level, something that was impossible before without closures or complex patterns.
It’s important to remember that private fields cannot be dynamically accessed via square bracket notation like this["#password"] or through Object.getOwnPropertyNames(). They don’t show up in normal property enumerations and are truly hidden from reflection APIs.
That means you can’t accidentally leak private data by iterating over properties or serializing objects. Here’s a quick demo of what’s visible on an instance:
const user = new User("alice", "hunter2");
console.log(Object.keys(user)); // ["username"]
console.log(Object.getOwnPropertyNames(user)); // ["username"]
console.log(user.#password); // SyntaxError
Private fields are a powerful addition, and once you get used to the syntax, they become your go-to for class internals instead of weak naming conventions or closures sprinkled around. They make it clear at the language level what’s meant to be private and what’s public.
That said, there are quirks and limitations that trip up developers coming from other languages or those used to traditional object property access. For example, you cannot declare private fields outside the class body, nor can you use them in object literals. They’re strictly a class feature.
Also, they cannot be accessed or modified dynamically, which means you lose some flexibility compared to public properties. That’s a trade-off for stronger encapsulation and security. If you need dynamic keys, private fields are not the tool for that job.
As you experiment with them, keep in mind the syntax rules: private fields must be declared at the top level of the class body, and you cannot redeclare a private field with the same name in a subclass. Trying to do so will cause a syntax error.
So, while the syntax is simple—just a hash prefix—the semantics behind private fields are deep, enforcing language-level encapsulation that was previously impossible in JavaScript. They let you write cleaner, more maintainable classes where internal state is truly hidden from the outside world.
Next up, we’ll look at why this matters for security and design, and how it protects your code from accidental or malicious interference, but first, be sure you’re comfortable with these syntax basics before moving on. The private field paradigm changes how you think about class design fundamentally, and that’s worth internalizing before you
Apple Watch SE 3 [GPS 40mm] Smartwatch with Starlight Aluminum Case with Starlight Sport Band - S/M. Fitness and Sleep Trackers, Heart Rate Monitor, Always-On Display, Water Resistant
$201.26 (as of June 2, 2026 22:39 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Why private fields matter for encapsulation and security
Private fields serve as a critical boundary that prevents accidental or intentional misuse of internal class data. If you’ve ever dealt with legacy codebases where object internals were freely accessible, you know how easily bugs can creep in when external code modifies state it shouldn’t.
With private fields, you’re explicitly telling the engine: “This data is off-limits to the outside world.” This is not just a convention; it’s a hard rule enforced at compile time. It reduces the attack surface for bugs and vulnerabilities, especially in large applications or libraries where you can’t control every consumer of your classes.
Consider a scenario where your class manages sensitive information, like API keys, tokens, or user credentials. Exposing these as public properties invites accidental leaks or tampering. Private fields keep this data locked down:
class ApiClient {
#apiKey;
constructor(apiKey) {
this.#apiKey = apiKey;
}
makeRequest(endpoint) {
// internally use #apiKey without risk of exposure
return fetch(endpoint, {
headers: { "Authorization": Bearer ${this.#apiKey} }
});
}
}
Because #apiKey is inaccessible outside the class, even if someone gets a hold of your ApiClient instance, they can’t simply read or overwrite the key. That is an important layer of defense.
Another subtle security benefit is that private fields prevent subclasses or external code from overriding or monkey-patching internal state variables. This means your class’s invariants and internal logic remain intact and predictable.
In contrast, public properties can be overwritten accidentally or maliciously:
class Session {
token = "initial-token";
}
const session = new Session();
session.token = "hacked-token"; // no barriers here
With private fields, that risk vanishes:
class SecureSession {
#token = "initial-token";
getToken() {
return this.#token;
}
}
const secureSession = new SecureSession();
secureSession.#token = "hacked-token"; // SyntaxError: Private field '#token' must be declared in an enclosing class
This kind of protection is especially vital when your classes are part of a public API or library, where you cannot trust the consumer to use your objects correctly.
From a design perspective, private fields help enforce the principle of least privilege. By hiding implementation details, you force consumers to interact with your objects only through well-defined public methods. This leads to cleaner, more maintainable code because internal changes won’t break external users.
Moreover, private fields enable better refactoring. Since no external code can rely on internals, you can safely change private field names, types, or logic without worrying about backward compatibility for those internals.
It’s worth highlighting that private fields also play well with modern JavaScript optimizations. Because the engine knows these fields are truly private and cannot be altered externally, it can optimize memory layout and access paths more aggressively than with normal properties.
However, this strict encapsulation comes with a tradeoff in flexibility. You cannot introspect private fields dynamically or serialize them directly. This means debugging sometimes requires additional public methods or custom inspection logic:
class Config {
#settings = { debug: true };
getSettings() {
// expose a safe copy or filtered view
return { ...this.#settings };
}
}
Attempting to peek inside private fields with reflection or serialization will yield empty or incomplete results, which can surprise developers unfamiliar with this behavior.
Finally, private fields align well with the future direction of JavaScript modules and encapsulation. They complement module scope privacy by enabling fine-grained control inside classes, making your code not just modular but also internally secure.
In summary, private fields matter because they make encapsulation real and enforceable, protect sensitive data from exposure or tampering, prevent subclasses and external code from interfering with internal state, and enable better design and optimization. That is a big step forward from the “underscore prefix” days where privacy was just a polite suggestion.
Understanding these benefits especially important before diving into common pitfalls, as misusing or misunderstanding private fields can lead to frustrating errors and subtle bugs. The next section will cover those traps and how to avoid them, but first, keep in mind that private fields are a tool for deliberate, explicit encapsulation, not a workaround for dynamic property access or flexible object shapes. They are a new mental model for how you design classes in JavaScript, and embracing that model unlocks cleaner and more secure code.
That means if you try to access or modify private fields outside their class context, it’s not just bad practice – it’s a syntax error that stops your program dead in its tracks. This hard barrier is what separates private fields from traditional properties and even from closures, which can be bypassed or leaked with enough effort.
For instance, consider this flawed attempt to copy private fields:
class Example {
#hidden = 123;
getHidden() { return this.#hidden; }
}
const obj = new Example();
const clone = { ...obj };
console.log(clone.#hidden); // SyntaxError
console.log(clone.hidden); // undefined
Here, spreading the object copies only public properties, ignoring private fields entirely. This behavior enforces privacy by design but requires you to provide explicit APIs for any data you want to expose or clone.
In essence, private fields make your class internals truly your own. They guard against accidental leaks, malicious tampering, and the confusion of implicit or undocumented access. This level of encapsulation is a fundamental building block for robust software, especially as JavaScript applications grow in size and complexity.
But don’t confuse private fields with security silver bullets. They protect your data at the language level, but if you expose private data through public methods carelessly, you still risk leaks. Always design your public interfaces thoughtfully to complement the privacy guarantees that private fields afford.
With that in mind, the next challenge is to understand where developers commonly stumble when using private fields – subtle syntax errors, misunderstandings about inheritance, and pitfalls around initialization – and how to avoid those traps to write clean, error-free code.
One common gotcha is attempting to use private fields before they’re declared or initialized. JavaScript requires private fields to be declared at the top level of the class body before any constructor or method uses them. For example:
class Foo {
constructor() {
console.log(this.#bar); // ReferenceError: Cannot access private field before initialization
}
#bar = 42;
}
This happens because private fields are initialized after the constructor starts running, so trying to read them too early throws an error. The fix is to declare private fields before the constructor and avoid accessing them too soon.
Another subtlety is that private fields are not inherited in the traditional sense. If you redeclare a private field with the same name in a subclass, JavaScript treats it as a completely different field, not an override. This can cause unexpected behavior:
class Parent {
#value = 1;
getValue() { return this.#value; }
}
class Child extends Parent {
#value = 2;
getChildValue() { return this.#value; }
}
const c = new Child();
console.log(c.getValue()); // 1
console.log(c.getChildValue()); // 2
Here, #value in Child is distinct from #value in Parent. They don’t share the same storage or semantics, which can be confusing.
Lastly, remember that private fields cannot be used as keys in computed property names or dynamic property access. You must use the literal #name syntax exactly as declared. Trying to do something like this will fail:
const fieldName = "#secret";
class MyClass {
[fieldName] = 123; // SyntaxError: Private field '#secret' must be declared in an enclosing class
}
Private fields are strictly lexical and static by design, which means you lose the flexibility of dynamic property names but gain guaranteed privacy and predictability in your class design. This tradeoff is intentional and key to their security model.
As you can see, private fields bring strong encapsulation and security benefits, but require careful attention to declaration order, inheritance semantics, and static naming. Getting these right will save you from confusing errors and help you write robust, maintainable classes that truly hide their internals.
Next, we’ll dive into practical tips for avoiding these common pitfalls and how to leverage private fields effectively in real-world codebases, but before that, make sure you have a solid grasp of these foundational rules because they are the bedrock for all
Common pitfalls when working with private fields and how to avoid them
One practical tip to avoid common pitfalls is to always declare all private fields at the very top of your class body, before the constructor or any methods. This ensures the fields are initialized before you try to use them anywhere inside the class.
class SafeExample {
#data;
#flag = false;
constructor(data) {
this.#data = data; // safe to assign here
}
toggleFlag() {
this.#flag = !this.#flag;
}
}
Declaring fields upfront avoids the “cannot access private field before initialization” error and makes your class structure clear and predictable.
Another common source of confusion is trying to access private fields from outside the class via indirect means, such as reflection, proxies, or dynamic property names. JavaScript’s private fields are designed to be absolutely inaccessible outside their lexical scope, so any attempt to circumvent this will fail:
class Hidden {
#secret = "top secret";
reveal() {
return this.#secret;
}
}
const obj = new Hidden();
console.log(obj["#secret"]); // undefined
console.log(Object.getOwnPropertyNames(obj)); // []
console.log(Object.getOwnPropertySymbols(obj)); // []
Even proxy traps cannot intercept access to private fields because they’re not properties on the object itself but part of an internal slot. This means you cannot hack around privacy by wrapping objects or enumerating keys.
Beware also of mixing private fields with public properties of the same name (minus the hash). While legal, it can lead to subtle bugs if you accidentally use one instead of the other:
class Confusing {
#value = 10;
value = 20;
getPrivate() {
return this.#value;
}
getPublic() {
return this.value;
}
}
const c = new Confusing();
console.log(c.getPrivate()); // 10
console.log(c.getPublic()); // 20
console.log(c.value); // 20
// c.#value; // SyntaxError: Private field '#value' must be declared in an enclosing class
Because private fields live in a separate namespace from public properties, you can end up with two different values that look related but are completely independent. This can confuse readers and maintainers, so it’s best to avoid naming collisions.
Another trap is forgetting that private fields cannot be re-declared or accessed in subclasses. Unlike protected members in some other languages, JavaScript private fields are strictly scoped to the declaring class. If you want to share internal state with subclasses, you need to use protected patterns with public or protected fields or methods instead.
class Parent {
#count = 0;
increment() {
this.#count++;
}
getCount() {
return this.#count;
}
}
class Child extends Parent {
incrementTwice() {
this.increment();
this.increment();
// this.#count++; // SyntaxError
}
}
If you need subclass access, consider protected-like behavior by using convention (underscore prefix) or controlled public methods rather than private fields.
Finally, avoid attempting to serialize or clone objects with private fields expecting those fields to be preserved. Since private fields do not appear in object keys or serialization outputs, you must provide explicit methods for exporting and importing private state:
class UserProfile {
#email;
constructor(email) {
this.#email = email;
}
toJSON() {
// expose only what is safe
return { email: this.#email };
}
static fromJSON(data) {
return new UserProfile(data.email);
}
}
const user = new UserProfile("[email protected]");
const json = JSON.stringify(user); // '{"email":"[email protected]"}'
const parsed = UserProfile.fromJSON(JSON.parse(json));
Relying on default serialization will silently drop private fields, leading to data loss or inconsistent state.
The key to avoiding pitfalls with private fields is understanding their strict lexical scoping, initialization order, and non-enumerable nature. Always declare them at the top of your class, avoid dynamic or external access, be mindful of inheritance boundaries, and provide explicit APIs for any interaction with private state.
