Skip to content

Private Fields

This lesson explains Private Fields with clear examples, use cases, and best practices so you can use modern JavaScript confidently in real projects.

ES6+ Private Fields Overview

Private fields let you declare class properties that are only accessible inside the class body. They use the # prefix syntax and provide true encapsulation in modern JavaScript without relying on naming conventions or closures.

Private fields are useful for hiding internal state, protecting sensitive values, and keeping public APIs clean while still using ES6 class syntax.

Feature Description
Introduced In ES2022
Syntax #fieldName
Access Only inside the declaring class
Types Instance fields, static fields, methods
Outside Access Not accessible on instances or subclasses
Common Uses Internal state, secrets, cache, validation

Basic Private Field Example

class BankAccount {
  #balance = 0;

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
    }
  }

  withdraw(amount) {
    if (
      amount > 0 &&
      amount <= this.#balance
    ) {
      this.#balance -= amount;
    }
  }

  getBalance() {
    return this.#balance;
  }
}

const account =
  new BankAccount();

account.deposit(500);
account.withdraw(120);

console.log(
  account.getBalance()
);

The #balance field is hidden from outside the class. Users interact through public methods instead of changing internal state directly.

How Private Fields Work

Private fields are declared with # at the class level. They can be read and written only from methods inside the same class declaration.

Step Description Example
Declare Field Add a # prefixed property. #count = 0
Use Inside Class Access with this.#count. this.#count++
Expose Safely Provide public methods or getters. getCount()
Block Outside Access External code cannot read the field. obj.#count fails
Protect State Keep implementation details private. Encapsulation

Private Fields in the Constructor

class User {
  #id;
  #email;

  constructor(id, email) {
    this.#id = id;
    this.#email = email;
  }

  getProfile() {
    return {
      id: this.#id,
      email: this.#email
    };
  }
}

const user =
  new User(1, "alex@example.com");

console.log(
  user.getProfile()
);

Private fields can be declared without an initial value and assigned inside the constructor, similar to public properties.

Private Fields with Getters and Setters

class Product {
  #price = 0;

  get price() {
    return this.#price;
  }

  set price(value) {
    if (value < 0) {
      throw new Error(
        "Price cannot be negative"
      );
    }

    this.#price = value;
  }
}

const product =
  new Product();

product.price = 49;
console.log(product.price);

A common pattern is to keep the real value in a private field while exposing controlled access through getters and setters.

Private Methods

class PasswordManager {
  #hash(value) {
    return `hashed-${value}`;
  }

  #validate(password) {
    return password.length >= 8;
  }

  save(password) {
    if (!this.#validate(password)) {
      throw new Error(
        "Password too short"
      );
    }

    return this.#hash(password);
  }
}

const manager =
  new PasswordManager();

console.log(
  manager.save("securePass123")
);

Private methods use the same # syntax. They are useful for helper logic that should not be part of the public class API.

Private Static Fields

class AppConfig {
  static #instanceCount = 0;

  constructor(appName) {
    this.appName = appName;
    AppConfig.#instanceCount += 1;
  }

  static getInstanceCount() {
    return AppConfig.#instanceCount;
  }
}

new AppConfig("Dashboard");
new AppConfig("Portal");

console.log(
  AppConfig.getInstanceCount()
);

Static private fields belong to the class itself, not to individual instances. Access them with ClassName.#field inside the class body.

Outside Access Is Not Allowed

class Counter {
  #value = 0;

  increment() {
    this.#value += 1;
  }

  getValue() {
    return this.#value;
  }
}

const counter =
  new Counter();

counter.increment();
console.log(counter.getValue());

// This causes a syntax error:
// console.log(counter.#value);

Trying to access #value outside the class results in a syntax error. Private fields are enforced by the language, not by convention alone.

Private Fields and Inheritance

class Animal {
  #energy = 100;

  eat(amount) {
    this.#energy += amount;
  }

  getEnergy() {
    return this.#energy;
  }
}

class Dog extends Animal {
  bark() {
    // Cannot access #energy here
    return "Woof!";
  }
}

const dog = new Dog();
dog.eat(20);

console.log(dog.getEnergy());
console.log(dog.bark());

Private fields are not inherited by subclasses. A child class cannot access a parent's private fields directly, which keeps encapsulation boundaries strict.

Private Fields vs Older Patterns

// Old pattern: naming convention

class OldUser {
  constructor(name) {
    this._name = name;
  }
}

// Old pattern: WeakMap

const names = new WeakMap();

class WeakUser {
  constructor(name) {
    names.set(this, name);
  }

  getName() {
    return names.get(this);
  }
}

// Modern pattern: private field

class ModernUser {
  #name;

  constructor(name) {
    this.#name = name;
  }

  getName() {
    return this.#name;
  }
}

Before private fields, developers used underscore prefixes or WeakMap to simulate privacy. Native private fields are cleaner and enforced by JavaScript itself.

Public Properties vs Private Fields

Feature Public Property Private Field
Syntax this.name this.#name
Outside Access Allowed Not allowed
Enforcement By convention only Language enforced
Best For Public API data Internal implementation state

Common Private Field Use Cases

  • Hiding internal counters, caches, or buffers.
  • Protecting sensitive values such as tokens or credentials.
  • Keeping validation logic inside private helper methods.
  • Exposing only safe public methods on domain classes.
  • Preventing direct mutation of internal object state.
  • Building reusable class libraries with clear APIs.
  • Tracking static class-level private metadata.

Private Field Best Practices

  • Use private fields for internal state that should never leak.
  • Expose data through intentional public methods or getters.
  • Keep private helper methods small and focused.
  • Do not use private fields for every property by default.
  • Name private fields clearly, such as #balance or #cache.
  • Validate input in setters before writing to private fields.
  • Check browser or runtime support for ES2022 class features.

Common Private Field Mistakes

  • Trying to access #field from outside the class.
  • Expecting subclasses to inherit private parent fields.
  • Using private fields without declaring them in the class body.
  • Confusing private fields with public properties.
  • Hiding everything privately when a simple public API would be clearer.
  • Forgetting that private fields require modern JavaScript support.
  • Mixing old underscore conventions with real private fields inconsistently.

Key Takeaways

  • Private fields use the # prefix inside ES6 classes.
  • They are accessible only within the declaring class body.
  • Private methods and static private fields follow the same rules.
  • They provide real encapsulation instead of naming conventions.
  • Use them to protect internal state and keep public APIs clean.

Pro Tip

Keep sensitive or mutable state in private fields and expose only what callers need through methods like deposit(), getBalance(), or controlled getters and setters.