Private Class Fields (#) and Static Blocks in JavaScript
Learn JavaScript private class fields with #, static blocks, private methods, and how they compare to closures and WeakMap patterns.
What you'll learn
- ✓Declaring and using private fields with the # syntax
- ✓Private methods, static private fields, and accessors
- ✓Class static initialization blocks for complex setup
Prerequisites
- •JavaScript classes and constructors
- •Basic OOP concepts
JavaScript classes now support truly private fields and methods using the # prefix. Unlike conventions like _privateField, these are enforced by the language — accessing them from outside the class throws a SyntaxError. Combined with static blocks, they give you complete control over class encapsulation.
Private instance fields
Prefix a field name with # to make it private.
class BankAccount {
#balance = 0;
#owner;
constructor(owner, initialBalance) {
this.#owner = owner;
this.#balance = initialBalance;
}
deposit(amount) {
if (amount <= 0) throw new Error("Amount must be positive");
this.#balance += amount;
return this.#balance;
}
withdraw(amount) {
if (amount > this.#balance) throw new Error("Insufficient funds");
this.#balance -= amount;
return this.#balance;
}
get balance() {
return this.#balance;
}
toString() {
return `${this.#owner}'s account: $${this.#balance}`;
}
}
const account = new BankAccount("Alice", 1000);
console.log(account.balance); // 1000 (via getter)
account.deposit(500);
console.log(account.balance); // 1500
// Cannot access private fields from outside
// account.#balance; // SyntaxError
// account.#owner; // SyntaxError
// Not visible in enumeration
console.log(Object.keys(account)); // []
console.log(JSON.stringify(account)); // "{}"
Private fields are not just a naming convention — they are a hard language guarantee. The # is part of the name itself, and the field is only accessible within the class body.
Private methods
Methods can also be private.
class PasswordValidator {
#minLength;
#requireSpecialChar;
constructor(options = {}) {
this.#minLength = options.minLength || 8;
this.#requireSpecialChar = options.requireSpecialChar ?? true;
}
#hasMinLength(password) {
return password.length >= this.#minLength;
}
#hasSpecialChar(password) {
return /[!@#$%^&*(),.?":{}|<>]/.test(password);
}
#hasUpperCase(password) {
return /[A-Z]/.test(password);
}
#hasNumber(password) {
return /\d/.test(password);
}
validate(password) {
const errors = [];
if (!this.#hasMinLength(password)) {
errors.push(`Must be at least ${this.#minLength} characters`);
}
if (!this.#hasUpperCase(password)) {
errors.push("Must contain an uppercase letter");
}
if (!this.#hasNumber(password)) {
errors.push("Must contain a number");
}
if (this.#requireSpecialChar && !this.#hasSpecialChar(password)) {
errors.push("Must contain a special character");
}
return { valid: errors.length === 0, errors };
}
}
const validator = new PasswordValidator({ minLength: 10 });
console.log(validator.validate("short"));
// { valid: false, errors: ["Must be at least 10 characters", ...] }
console.log(validator.validate("SecurePass1!"));
// { valid: true, errors: [] }
Private accessors (getters/setters)
You can make getters and setters private too.
class Temperature {
#celsius;
constructor(celsius) {
this.#celsius = celsius;
}
get #fahrenheit() {
return this.#celsius * 9 / 5 + 32;
}
set #fahrenheit(f) {
this.#celsius = (f - 32) * 5 / 9;
}
// Public interface
get celsius() {
return this.#celsius;
}
get fahrenheit() {
return this.#fahrenheit; // uses private getter
}
display() {
return `${this.#celsius}C / ${this.#fahrenheit}F`;
}
}
const temp = new Temperature(100);
console.log(temp.display()); // "100C / 212F"
Static private fields
Private fields can be static, shared across all instances but only accessible within the class.
class DatabaseConnection {
static #instance = null;
static #connectionCount = 0;
#id;
#connected = false;
constructor(config) {
DatabaseConnection.#connectionCount++;
this.#id = DatabaseConnection.#connectionCount;
this.config = config;
}
static getInstance(config) {
if (!DatabaseConnection.#instance) {
DatabaseConnection.#instance = new DatabaseConnection(config);
}
return DatabaseConnection.#instance;
}
static getConnectionCount() {
return DatabaseConnection.#connectionCount;
}
connect() {
this.#connected = true;
console.log(`Connection #${this.#id} established`);
}
}
const db1 = DatabaseConnection.getInstance({ host: "localhost" });
const db2 = DatabaseConnection.getInstance({ host: "remote" });
console.log(db1 === db2); // true (singleton)
console.log(DatabaseConnection.getConnectionCount()); // 1
Class static initialization blocks
Static blocks run once when the class is evaluated, giving you a place for complex initialization logic.
class Config {
static #settings;
static #environment;
static {
// Complex initialization that cannot be done inline
try {
Config.#settings = JSON.parse(
typeof process !== "undefined"
? process.env.APP_CONFIG || "{}"
: "{}"
);
} catch {
Config.#settings = {};
}
Config.#environment =
Config.#settings.env || (typeof window !== "undefined" ? "browser" : "node");
}
static get(key) {
return Config.#settings[key];
}
static get environment() {
return Config.#environment;
}
}
console.log(Config.environment); // "browser" or "node"
Multiple static blocks
You can have multiple static blocks. They run in order of appearance.
class Registry {
static #items = new Map();
static #initialized = false;
static {
// First block: register defaults
Registry.#items.set("format", "json");
Registry.#items.set("timeout", 5000);
Registry.#items.set("retries", 3);
}
static {
// Second block: override from environment
if (typeof process !== "undefined" && process.env.TIMEOUT) {
Registry.#items.set("timeout", parseInt(process.env.TIMEOUT));
}
Registry.#initialized = true;
}
static get(key) {
return Registry.#items.get(key);
}
static isInitialized() {
return Registry.#initialized;
}
}
console.log(Registry.get("timeout")); // 5000
console.log(Registry.isInitialized()); // true
Checking for private fields with in
You can use the in operator to check if an object has a private field. This enables brand checking.
class Color {
#red;
#green;
#blue;
constructor(r, g, b) {
this.#red = r;
this.#green = g;
this.#blue = b;
}
static isColor(obj) {
return #red in obj; // brand check
}
equals(other) {
if (!Color.isColor(other)) {
throw new TypeError("Expected a Color instance");
}
return (
this.#red === other.#red &&
this.#green === other.#green &&
this.#blue === other.#blue
);
}
toString() {
return `rgb(${this.#red}, ${this.#green}, ${this.#blue})`;
}
}
const red = new Color(255, 0, 0);
console.log(Color.isColor(red)); // true
console.log(Color.isColor({ r: 255 })); // false
Private fields vs older patterns
Before: WeakMap pattern
const _balance = new WeakMap();
class OldAccount {
constructor(balance) {
_balance.set(this, balance);
}
getBalance() {
return _balance.get(this);
}
}
Before: Closure pattern
function createAccount(initialBalance) {
let balance = initialBalance; // truly private via closure
return {
deposit(amount) { balance += amount; },
getBalance() { return balance; },
};
}
Now: Private fields
class Account {
#balance;
constructor(balance) {
this.#balance = balance;
}
get balance() {
return this.#balance;
}
}
Private fields combine the true privacy of closures with the familiar class syntax and the performance of regular property access.
Inheritance and private fields
Private fields are not inherited by subclasses. Each class has its own private namespace.
class Animal {
#name;
constructor(name) {
this.#name = name;
}
getName() {
return this.#name;
}
}
class Dog extends Animal {
#breed;
constructor(name, breed) {
super(name);
this.#breed = breed;
// this.#name; // SyntaxError -- cannot access parent's private field
}
describe() {
return `${this.getName()} is a ${this.#breed}`;
}
}
const dog = new Dog("Rex", "Labrador");
console.log(dog.describe()); // "Rex is a Labrador"
The subclass must use the parent’s public or protected interface to access parent data.
Summary
- Private fields (
#field) provide true encapsulation enforced by the language. - Private methods (
#method()) keep implementation details hidden. - Static private fields share data across instances while keeping it inaccessible externally.
- Static blocks enable complex class initialization logic.
- Use
#field in objfor brand checking (verifying an object is a genuine instance). - Private fields are not inherited — subclasses use the parent’s public API.
- This replaces older patterns (WeakMap, closures, underscore conventions) with cleaner, faster syntax.
Private class fields are one of the most impactful additions to JavaScript classes. They eliminate the need for workarounds and give you genuine encapsulation that the language enforces at the syntax level.
Related articles
- JavaScript JavaScript Event Loop Explained: How Async Code Really Works
Understand how the JavaScript event loop handles async operations including the call stack, microtasks, macrotasks, and execution order with practical examples.
- JavaScript JavaScript structuredClone: The Modern Deep Copy Solution
Learn how to use JavaScript structuredClone for deep copying objects, when it beats JSON.parse(JSON.stringify()), and what types it supports and cannot handle.
- JavaScript JavaScript Design Patterns Every Developer Should Know
Learn the most useful JavaScript design patterns with practical examples including Singleton, Observer, Factory, Strategy, and more for cleaner code.
- JavaScript How to Find and Fix JavaScript Memory Leaks
Learn how to detect, diagnose, and fix JavaScript memory leaks using Chrome DevTools heap snapshots, allocation timelines, and common leak patterns.