Private and protected properties and methods

One of the biggest ideas in object-oriented programming is drawing a hard line between what an object shows the outside world and what it keeps to itself. Once your program grows past a toy example, that boundary is what keeps it maintainable.

Before we touch any code, step away from the screen for a moment and think about physical objects.

Most machines you use every day are complicated inside. You get away with using them anyway because the complicated parts are sealed off from you.

A real-life example

Take a coffee machine. From the outside it is friendly: a button, a small display, a couple of openings, and — most importantly — coffee.

READYbuttoncoffee
A coffee machine from the outside: one button, a small display, a spout, and coffee.

Open it up, though, and it looks like this (straight from a repair manual):

boilerpumpheatertubes & wiring
Open it up and there are dozens of parts, wired and plumbed together.

Dozens of parts. And yet you never have to think about a single one of them to make a cup of coffee.

Coffee machines tend to last. You can run one for years, and you only crack it open when something breaks — at which point you hand it to someone who repairs machines for a living.

That reliability comes from a design choice: every internal part is carefully arranged and hidden behind a shell.

Strip that shell off and the machine becomes both confusing (which of these do I press?) and hazardous (some of those parts carry current).

Objects in code work the same way. The difference is that we hide the internals with language features and shared conventions instead of a plastic cover.

external interface
button
display
internal interface (hidden)
boiler
heater
pump
The shell is the boundary: the outside is simple, the inside stays sealed.

Internal and external interface

In object-oriented design, an object’s properties and methods fall into two camps:

  • The internal interface — methods and properties that other methods of the same class can reach, but code outside the class cannot.
  • The external interface — methods and properties that outside code is allowed to touch.

Back to the coffee machine: the boiler tube, the heating element, and everything else buried inside make up its internal interface.

Those parts exist so the machine can do its job, and they lean on each other. The boiler tube bolts onto the heating element, for example.

From the outside, the cover seals all of that away. You cannot reach in, and you do not need to. You operate the machine through its external interface.

So to use an object, the external interface is all you need. You can stay blissfully ignorant of the wiring inside, and that is exactly the point.

With the analogy out of the way, here is how JavaScript maps onto it. An object’s fields (both properties and methods) come in these kinds:

  • Public: reachable from anywhere. This is the external interface. Everything you have written so far has been public.
  • Private: reachable only from inside the class. This is the internal interface.

Plenty of other languages add a third kind: protected fields. They behave like private, but with one extra allowance — classes that inherit from yours can also reach them. That makes them handy for the internal interface, and in practice they show up more often than strictly private fields, because subclasses usually do want a way in.

JavaScript has no protected keyword at the language level. But the pattern is useful enough that developers emulate it with a naming convention.

fieldoutside codethe classsubclasses
public xyesyesyes
protected _xno*yesyes
private #xnoyesno
Who can reach what: public is open to all; protected is class + subclasses; private is class only.

The asterisk on protected matters: nothing stops outside code from reading _x. The convention says “keep out,” but the language shrugs.

Now let’s build a coffee machine in JavaScript that uses all of these. A real machine has hundreds of parts; we will keep the model small so the ideas stay in focus.

Protecting “waterAmount”

Start with the simplest possible class:

class CoffeeMachine {
  waterAmount = 0; // the amount of water inside

  constructor(power) {
    this.power = power;
    alert( `Created a coffee-machine, power: ${power}` );
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(90);

// add water
coffeeMachine.waterAmount = 200;

At this point waterAmount and power are both public. Any code can read them and, more worryingly, write any value into them from the outside.

Let’s tighten up waterAmount. Water can’t be negative, so we want a gate that rejects bad values instead of letting anyone assign directly.

By convention, a protected property’s name starts with an underscore _.

The language does not police this. It’s a signal between developers: a name beginning with _ is off-limits from the outside, and reading or writing it from unrelated code is considered a mistake.

So the raw storage becomes _waterAmount, and a public getter/setter pair guards it:

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0) {
      value = 0;
    }
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(90);

// add water
coffeeMachine.waterAmount = -10; // _waterAmount will become 0, not -10

Now every write goes through the setter, so a negative amount can never land in storage.

outside
.waterAmount = -10
set waterAmount
clamp < 0 to 0
_waterAmount
0
The setter is a checkpoint between outside code and the protected _waterAmount.

Read-only “power”

For power, we want something stricter: set it once when the machine is built, then never again. Some properties are like that — decided at creation and fixed for life.

Power fits perfectly. A coffee machine’s wattage doesn’t change while it runs.

The trick is to provide a getter and no setter:

class CoffeeMachine {
  // ...

  constructor(power) {
    this._power = power;
  }

  get power() {
    return this._power;
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(90);

alert(`Power is: ${coffeeMachine.power}W`); // Power is: 90W

coffeeMachine.power = 30; // Error (no setter)

Reading works; writing throws. Well — it throws in strict mode. Class bodies always run in strict mode, so inside a class assigning to a getter-only property raises a TypeError. In sloppy mode the same assignment would fail silently, which is one more reason strict mode is the default here.

Private “#waterLimit”

Modern JavaScript ships real, language-level private fields (a finished proposal, now part of the standard and supported across current engines).

A private field’s name starts with #. It can be touched only from inside the very class that declares it.

Here’s a private #waterLimit property alongside a private helper #fixWaterAmount that keeps values in range:

class CoffeeMachine {
  #waterLimit = 220;

  #fixWaterAmount(value) {
    if (value < 0) return 0;
    if (value > this.#waterLimit) return this.#waterLimit;
  }

  setWaterAmount(value) {
    this.#waterLimit = this.#fixWaterAmount(value);
  }

}

let coffeeMachine = new CoffeeMachine();

// can't access privates from outside of the class
coffeeMachine.#fixWaterAmount(123); // Error
coffeeMachine.#waterLimit = 900; // Error

The # is not decoration. It marks the field as private at the language level, and the engine refuses any access from outside the class or from a subclass. Reaching for coffeeMachine.#waterLimit from the outside is a syntax error — the code won’t even run.

Private and public names live in separate spaces. A class can hold both a private #waterAmount and a public waterAmount at once, and they won’t collide.

That lets you use the public name as an accessor for the private storage:

class CoffeeMachine {

  #waterAmount = 0;

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

  set waterAmount(value) {
    if (value < 0) value = 0;
    this.#waterAmount = value;
  }
}

let machine = new CoffeeMachine();

machine.waterAmount = 100;
alert(machine.#waterAmount); // Error
machine.waterAmount
public accessor
this.#waterAmount
private storage
machine.#waterAmount
✗ from outside
Public waterAmount is the door; private #waterAmount is the locked room behind it.

The difference from the underscore convention is enforcement. Protected _ relies on everyone playing nice; private # is guaranteed by the runtime. That guarantee is genuinely valuable.

The catch is that the guarantee is absolute — even for your own subclasses. Extend CoffeeMachine and you lose direct access to #waterAmount; you have to go through the public waterAmount getter/setter instead:

class MegaCoffeeMachine extends CoffeeMachine {
  method() {
    alert( this.#waterAmount ); // Error: can only access from CoffeeMachine
  }
}

For a lot of real designs that’s too strict. When you extend a class, you often have a good reason to reach into its internals. That’s why the protected-with-underscore convention stays popular despite having zero enforcement — it leaves the door open for subclasses.

Summary

Splitting the internal interface from the external one has a name in OOP: encapsulation.

Here is what it buys you.

Protection for users, so they don’t shoot themselves in the foot

Picture a team sharing a coffee machine from the “Best CoffeeMachine” company. It works well — but someone pried off the protective cover, so the guts are now exposed.

Most people still use it as intended. Then Maya, convinced she knows better, tweaks something inside. Two days later the machine dies.

You wouldn’t really blame Maya. You’d blame whoever removed the cover and left those internals reachable in the first place.

Code is the same. When outside code changes something that was never meant to be changed from outside, the fallout is unpredictable — and it lands on whoever exposed the internals.

Supportable

Software is messier than a physical machine, because you don’t buy it once and walk away. It keeps changing — fixes, features, rewrites.

When the internal interface is strictly walled off, the class author can rework internal properties and methods freely, without even telling users.

As the author, that freedom is a relief: private methods can be renamed, have their parameters reshaped, or be deleted outright, because no external code was allowed to depend on them.

For users, an upgrade might replace the entire inside of the class, yet stay a drop-in change as long as the external interface holds steady.

Hiding complexity

People love things that are simple to use, whatever chaos hides underneath. Programmers are no exception.

A hidden implementation paired with a small, well-documented external interface is a joy to work with.

To hide the internals, reach for protected or private fields:

  • Protected fields start with _. Convention only, not enforced by the language. The agreement is that such a field is touched solely from its own class and its subclasses.
  • Private fields start with #. The language itself guarantees they’re reachable only from inside the declaring class.

Private fields are supported in every current JavaScript engine. For very old environments they can be transpiled or polyfilled.