JavaScript getters and setters are the feature that lets an object expose a property while still controlling what happens when that property is read or written. They are not renamed methods. A method is a function you call with parentheses; a getter is an accessor — a different kind of property entirely — and once you can see the difference in a property descriptor, you will know exactly when to reach for each.
The example running through this page is a temperature reading, because it has a natural computed value (Fahrenheit) and a natural validation rule (it cannot be below absolute zero). Both of those are reasons to want an accessor rather than a plain field.
What is a getter and a setter, and when do you want one?
A getter is written with the get keyword before a name. A setter is written with set before the same name. The two halves are two views of a single property: reading it runs the getter, writing it runs the setter.
class Temperature {
#celsius;
constructor(celsius) {
this.celsius = celsius;
}
get celsius() {
return this.#celsius;
}
set celsius(value) {
this.#celsius = value;
}
get fahrenheit() {
return this.#celsius * 9 / 5 + 32;
}
}
const morning = new Temperature(20);
console.log(morning.celsius);
console.log(morning.fahrenheit);
morning.celsius = 25;
console.log(morning.celsius);
console.log(morning.fahrenheit);
No parentheses appear at the call site. morning.celsius is a property read, and morning.celsius = 25 is a property write that happens to run code. fahrenheit is a pure computation with no setter at all, so it is read-only in practice — trying to assign to it would fail because there is no setter to receive the value.
The decision rule is short: if the value is a property of the object — something with a name that people expect to see in an inspector — use a property, backed by a getter or setter. If it is an action, or it takes arguments, use a method. temperature.fahrenheit reads like a property, which is why it is one. temperature.convertTo("kelvin") would be an action, so it belongs in a method.
Are getters just methods wearing a different hat?
No, and the difference is visible in the property descriptor. A method is a property whose value is a function. A getter is a property whose value is a pair of functions stored under get and set keys.
class Temperature {
#celsius = 0;
get celsius() {
return this.#celsius;
}
set celsius(value) {
this.#celsius = value;
}
toString() {
return this.celsius + " C";
}
}
const descriptor = Object.getOwnPropertyDescriptor(Temperature.prototype, "celsius");
console.log(typeof descriptor.get);
console.log(typeof descriptor.set);
console.log("value" in descriptor);
console.log(descriptor.enumerable);
console.log(Object.getOwnPropertyNames(Temperature.prototype));
console.log(Object.keys(Temperature.prototype));
descriptor.get and descriptor.set are both functions, but the descriptor has no value key at all — that is the giveaway that this is an accessor and not a method. Accessors defined in a class body are non-enumerable, so descriptor.enumerable is false and Object.keys on the prototype returns an empty array. Object.getOwnPropertyNames still lists every member, including constructor, celsius and toString.
Accessors live on the prototype, so every instance shares them, exactly like methods. What makes them different is what happens at the call site: a method is a function you invoke, an accessor is a property the engine intercepts.
How do you validate a value before it is stored?
Put the check in the setter. Then every write goes through the same rule, whether it comes from your own code, a form handler, or a test.
class Temperature {
#celsius = 0;
get celsius() {
return this.#celsius;
}
set celsius(value) {
if (typeof value !== "number" || Number.isNaN(value)) {
throw new TypeError("celsius must be a number");
}
if (value < -273.15) {
throw new RangeError("below absolute zero");
}
this.#celsius = value;
}
}
const probe = new Temperature();
probe.celsius = 18;
console.log(probe.celsius);
try {
probe.celsius = "warm";
} catch (err) {
console.log(err.constructor.name + ": " + err.message);
}
try {
probe.celsius = -400;
} catch (err) {
console.log(err.constructor.name + ": " + err.message);
}
Assigning the string warm throws a TypeError with the message celsius must be a number. Assigning -400 throws a RangeError with the message below absolute zero. Using two different error types matters: the caller can tell "you sent the wrong kind of thing" apart from "you sent the right kind of thing with a value outside the allowed range".
Note that the failed writes left the object untouched — the check happens before the assignment, so probe.celsius is still 18. Throwing is only half the story though: some callers would rather be handed a corrected value than an exception. If the setter silently coerces or clamps, the caller gets no signal that their input was wrong, which is its own kind of bug.
What happens when you write to a property that only has a getter?
It depends on whether the writing code is strict. Class bodies are always strict, so code inside a class method gets an immediate TypeError. Loose code gets a silent no-op instead, which is far more dangerous because nothing tells you the write failed.
"use strict";
class Reading {
#value = 0;
get value() {
return this.#value;
}
}
const reading = new Reading();
console.log(reading.value);
try {
reading.value = 42;
} catch (err) {
console.log(err.constructor.name + ": " + err.message);
}
console.log(reading.value);
The value starts at 0, the assignment throws a TypeError reading Cannot set property value of #<Reading> which has only a getter, and the value is still 0 afterwards. If you remove the "use strict" line the same program prints 0 and then 0 again with no error at all — the write is quietly discarded. That difference is worth knowing before you spend an afternoon wondering why a value never changes.
When should a method be static instead of an instance method?
A static method belongs to the class itself rather than to any instance. It is the right home for a factory that builds instances, and for helpers that need no instance at all.
class Coordinate {
constructor(x, y) {
this.x = x;
this.y = y;
}
static origin() {
return new Coordinate(0, 0);
}
static fromDegrees(degrees, radius = 1) {
const radians = (degrees * Math.PI) / 180;
return new Coordinate(radius * Math.cos(radians), radius * Math.sin(radians));
}
distanceTo(other) {
return Math.hypot(this.x - other.x, this.y - other.y);
}
toString() {
return "(" + this.x + ", " + this.y + ")";
}
}
const a = Coordinate.origin();
const b = Coordinate.fromDegrees(90, 10);
console.log(a.toString());
console.log(b.x.toFixed(4), b.y.toFixed(4));
console.log(a.distanceTo(b).toFixed(4));
console.log(typeof Coordinate.origin, typeof a.distanceTo);
Coordinate.origin() returns a point at (0, 0), and Coordinate.fromDegrees(90, 10) returns a point whose coordinates are approximately 0.0000 and 10.0000. The distance between them is 10.0000. The last line shows the split clearly: Coordinate.origin is a static method and a.distanceTo is an instance method, and both are functions.
Two practical notes. First, static methods are inherited by subclasses, so a child class gets its parent's factories for free unless it overrides them. Second, an instance method is not automatically bound to its instance — pulling const d = a.distanceTo off the object and calling d(b) loses the this that the method expects. That is the subject of the next lesson.
The short version of getters, setters and methods
- A getter and a setter are one property with two halves; a method is a property holding a function.
- Call sites tell them apart:
obj.valuefor an accessor,obj.value()for a method. - Accessors live on the prototype, so they are shared, and they are non-enumerable in class bodies.
- Put validation in the setter so every write passes the same gate, and choose the error type to match the kind of mistake.
- A getter with no setter makes the property read-only, and strict code turns a bad write into a loud
TypeError. - Use static methods for factories and helpers that need no instance.
Try it yourself: write a class Duration that stores a private #seconds field and exposes it through a seconds getter and setter, plus a computed minutes getter that returns #seconds / 60. Make the setter reject negative numbers with a RangeError and reject non-numbers with a TypeError. Add a static factory static fromMinutes(mins) that builds a duration, and an instance method add(other) that returns a new duration with the summed seconds. Then run Object.getOwnPropertyDescriptor(Duration.prototype, "seconds") and confirm it has get and set but no value key.