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.
Open it up, though, and it looks like this (straight from a repair manual):
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.
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.
| field | outside code | the class | subclasses |
|---|---|---|---|
public x | yes | yes | yes |
protected _x | no* | yes | yes |
private #x | no | yes | no |
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.
.waterAmount = -10
clamp < 0 to 0
0
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
public accessor
private storage
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.