TL;DR
- Getters and setters allow you to control access to class properties.
- Setters are used to validate and sanitize data before it’s stored.
- Getters are used to control how data is returned — sometimes hiding or formatting sensitive information.
- It’s a way to encapsulate logic and protect internal state, especially when working with classes.
Intro
The question that could save your car from flying off at Eau Rouge: how do I protect my classes in JavaScript?
We’ve already walked through the power of this
, constructor functions and classes, and one of the main reasons I use to justify classes was Security. As Uncle Ben once said (probably to a junior dev): "With great power comes great responsibility.". But although I said it is a good practice due to security reasons, there are still some basics to be taught about protecting your classes.
Back to my hyper-focus on F1, you wouldn’t want to give a junior engineer direct access to the core setup of a McLaren F1 car, would you?
In JavaScript, if you’re not careful, you’re doing exactly that — letting anyone read or change values inside your class without any kind of validation.
And that’s where getters and setters come in. They are special methods from classes that allow you to encapsulate and protect your data.
Protecting Inputs
Let’s say you’re building a class to represent a Formula 1 car. One of the critical parameters is the differential adjustment on throttle. Don’t worry if you don’t know what that is — just know this:
👉 It must be a number between 0 and 100.
But without validation, your class can be abused like this:
const mclaren = new F1Car();
mclaren.diffOnThrottle = "Invalid value";
That's like letting your cousin, who has never seen F1, adjust Lando Norris' setup for Monaco.
So instead, let’s protect it using private fields and setters:
class F1Car {
#diffOnThrottle = 50; // private field
set diffOnThrottle(value) {
if (typeof value !== 'number' || value < 0 || value > 100) {
throw new Error("Differential on throttle must be a number between 0 and 100.");
}
this.#diffOnThrottle = value;
}
}
mclaren.diffOnThrottle = "Invalid value"; // Uncaught Error: Differential on throttle must be a number between 0 and 100.
mclaren.diffOnThrottle = 70 // Will work
Now, anytime someone tries to change the diff setup, the setter makes sure the value is valid. You’re no longer handing out the keys to the pit wall.
Controlling outputs
One thing you will notice now is that, if you try to console.log
the value of the diffOnThrottle
, it won't work.
console.log(this.diffOnThrottle) //undefined
This happens because we are using a private field (#) on our class.
Without a getter, trying to access mclaren.diffOnThrottle just gives you undefined, because the actual field is private (#diffOnThrottle). This means that only the class has access to it. In order to be able to see the value, we need to use getters.
class F1Car {
#diffOnThrottle = 50; // private field
set diffOnThrottle(value) {
if (typeof value !== 'number' || value < 0 || value > 100) {
throw new Error("Differential on throttle must be a number between 0 and 100.");
}
this.#diffOnThrottle = value;
}
get diffOnThrottle() {
return `${this.#diffOnThrottle}%`;
}
}
const mclaren = new F1Car();
console.log("Initial diff on throttle:", mclaren.diffOnThrottle);
mclaren.diffOnThrottle = 70 // 70
console.log("New diff on throttle:", mclaren.diffOnThrottle);
This way, not only you can choose what to expose from you classes by creating or not creating a getter for an attribute, but you can also define how the return value will be shown. For example, I could have my getter written like this:
get diffOnThrottle() {
return `Current Diff on Throttle: ${this.#diffOnThrottle}%`;
}
So that every time I call the mclaren.diffOnThrottle
, it will show as "Current Diff on Throttle: x", where X is the current value.
Bonus: Getters and Setters Without Classes?
You don’t need to use classes to have getters and setters — you can use Object.defineProperty()
on plain objects too.
Here’s the same idea of protecting a field, but using a plain object instead of a class:
const sauber = {
_fuel: 0,
};
Object.defineProperty(sauber, 'fuel', {
get() {
return `${this._fuel} liters`;
},
set(value) {
if (typeof value !== 'number' || value < 0 || value > 110) {
throw new Error("Fuel must be a number between 0 and 110 liters.");
}
this._fuel = value;
},
});
sauber.fuel = 80;
console.log(sauber.fuel); // "80 liters"
sauber.fuel = "full"; // Error
This works the same way as in a class, but under the hood — more manual, but still useful when you don't want to write a class.
Another Bonus: Getters Can Compute Things Too
Setters usually validate or sanitize data, but getters can also do more than just return raw values — they can compute derived information.
For example, let’s say your car tracks how much fuel it has and how much it consumes per lap. You could create a getter that tells how many laps you can still run:
class F1Car {
#fuel = 100; // liters
#consumptionPerLap = 2.5; // liters per lap
set fuel(value) {
if (typeof value !== 'number' || value < 0 || value > 110) {
throw new Error("Fuel must be a number between 0 and 110.");
}
this.#fuel = value;
}
set consumptionPerLap(value) {
if (typeof value !== 'number' || value <= 0) {
throw new Error("Consumption must be a positive number.");
}
this.#consumptionPerLap = value;
}
get remainingLaps() {
return Math.floor(this.#fuel / this.#consumptionPerLap);
}
}
const ferrari = new F1Car();
ferrari.fuel = 75;
ferrari.consumptionPerLap = 2.5;
console.log("Remaining laps:", ferrari.remainingLaps); // 30
So instead of exposing raw data, the getter gives you meaningful insight — kind of like telemetry dashboards in real F1 teams.
Recap
- Setters let you validate, sanitize, or transform data before storing it.
- Getters let you format, restrict, or compute values before returning them.
- Both allow you to encapsulate logic inside your class — something every serious engineer (code or F1) should care about.
- Protecting internal state is key to making your classes secure, predictable, and future-proof.
Wrapping up
When you're designing classes, think like an F1 engineer. Not everything should be adjustable by just anyone. Not everything should be visible to the public.
Encapsulation is not just about being "clean" — it's about building robust systems that don’t crash when someone types "banana"
into a numeric field.
So... next time you're coding your class, ask yourself:
Am I building a well-guarded and projected McLaren, or a messy Sauber with an open fuel cap?
💬 Got a clever way of using getters or setters in your projects?
Have you ever saved a bug thanks to input validation or locked down your data like it was FIA telemetry?
Drop a comment — I’d love to see how you’re protecting your objects in the wild!
📬 Enjoying the series? Save it, share it with your dev squad, or that one friend who still assigns "banana" to numeric fields. 😅
Let’s keep leveling up together.
👉 Follow me @matheusjulidori for the next episodes of Do You Know How It Works?
🎥 Subscribe to my YouTube channel to be the first ones to watch my new project as soon as it is released!
Next up: Higher-Order Functions —
Why do we call map, filter, and reduce higher? And what makes a function worthy of the title?
Let’s break it down in plain JavaScript — and probably with another F1 analogy.
Top comments (0)