docs/beyond/concepts/getters-setters.mdx
How do you create a property that calculates its value on the fly? What if you want to validate data every time someone assigns a value? And how do you make a property that looks normal but does something behind the scenes?
const user = {
firstName: "Alice",
lastName: "Smith",
// This looks like a property, but it's actually a function
get fullName() {
return `${this.firstName} ${this.lastName}`
}
}
// Access it like a property — no parentheses!
console.log(user.fullName) // "Alice Smith"
// It recalculates every time
user.firstName = "Bob"
console.log(user.fullName) // "Bob Smith"
Getters and setters are special functions that look and behave like regular properties. A getter is called when you read a property. A setter is called when you assign to it. They let you add logic to property access without changing how the property is used.
<Info> **What you'll learn in this guide:** - What getters and setters are and why they're useful - How to define them in object literals and classes - The backing property pattern to avoid infinite loops - Using [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) for accessor descriptors - Common use cases: computed values, validation, encapsulation - Getter-only (read-only) and setter-only (write-only) properties - How getters and setters work with inheritance - Performance considerations and caching patterns </Info> <Warning> **Prerequisite:** This guide builds on [Property Descriptors](/beyond/concepts/property-descriptors). Understanding data vs accessor descriptors will help you get the most from this guide. </Warning>Getters and setters are functions disguised as properties. When you access a getter, JavaScript calls the function and returns its result. When you assign to a setter, JavaScript calls the function with the assigned value. The key difference from regular methods is the syntax: no parentheses. According to the ECMAScript specification, getters and setters are defined as special method types within object literals and class bodies, creating accessor property descriptors rather than data descriptors.
const circle = {
radius: 5,
// Getter — called when you READ circle.area
get area() {
return Math.PI * this.radius ** 2
},
// Setter — called when you WRITE circle.diameter = value
set diameter(value) {
this.radius = value / 2
}
}
// Getters: access like a property
console.log(circle.area) // 78.53981633974483
console.log(circle.area) // Same — recalculates each time
// Setters: assign like a property
circle.diameter = 20
console.log(circle.radius) // 10 (setter updated it)
console.log(circle.area) // 314.159... (getter recalculates)
The difference is purely syntactic, but it affects how you think about and use the property:
const rectangle = {
width: 10,
height: 5,
// Method — requires parentheses
calculateArea() {
return this.width * this.height
},
// Getter — no parentheses
get area() {
return this.width * this.height
}
}
// Method call
console.log(rectangle.calculateArea()) // 50
// Getter access
console.log(rectangle.area) // 50
// Forgetting parentheses on method returns the function itself
console.log(rectangle.calculateArea) // [Function: calculateArea]
// But getters are called automatically
console.log(rectangle.area) // 50 (not the function)
Think of an object as a vending machine. Regular properties are like items sitting on a shelf. You can see them and grab them directly. But getters and setters add a layer of interaction.
┌─────────────────────────────────────────────────────────────────────────┐
│ GETTERS & SETTERS: THE VENDING MACHINE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ REGULAR PROPERTY GETTER │
│ ──────────────── ────── │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ SHELF │ │ DISPLAY │ │
│ │ ┌─────┐ │ │ ┌─────┐ │ │
│ │ │ 🥤 │ │ ← Grab directly │ │ ?? │ │ ← Press button │
│ │ └─────┘ │ │ └─────┘ │ to dispense │
│ └─────────────┘ │ ▼ │ │
│ obj.drink │ ┌─────┐ │ │
│ │ │ 🥤 │ │ ← Machine makes │
│ │ └─────┘ │ it for you │
│ └─────────────┘ │
│ obj.freshDrink (getter) │
│ │
│ SETTER │
│ ────── │
│ ┌─────────────────────────────────────┐ │
│ │ COIN SLOT │ │
│ │ ┌─────┐ │ │
│ │ │ 💰 │ → Insert money │ ← Machine validates, │
│ │ └─────┘ (setter called) │ processes, stores │
│ │ ▼ │ │
│ │ ┌──────────┐ │ │
│ │ │ VALIDATE │ │ │
│ │ │ STORE │ │ │
│ │ └──────────┘ │ │
│ └─────────────────────────────────────┘ │
│ obj.balance = 5 (setter) │
│ │
│ The machine handles complexity. You just interact with a simple │
│ interface — but behind the scenes, code runs. │
│ │
└─────────────────────────────────────────────────────────────────────────┘
The most common way to define getters and setters is in object literals using the get and set keywords.
const user = {
firstName: "Alice",
lastName: "Smith",
// Getter
get fullName() {
return `${this.firstName} ${this.lastName}`
},
// Setter
set fullName(value) {
const parts = value.split(" ")
this.firstName = parts[0]
this.lastName = parts[1] || ""
}
}
// Using the getter
console.log(user.fullName) // "Alice Smith"
// Using the setter
user.fullName = "Bob Jones"
console.log(user.firstName) // "Bob"
console.log(user.lastName) // "Jones"
You can use computed property names with getters and setters:
const propName = "status"
const task = {
_status: "pending",
get [propName]() {
return this._status.toUpperCase()
},
set [propName](value) {
this._status = value.toLowerCase()
}
}
console.log(task.status) // "PENDING"
task.status = "DONE"
console.log(task.status) // "DONE"
console.log(task._status) // "done"
When a getter/setter needs to store a value, you need a separate "backing" property. By convention, this is prefixed with an underscore:
const account = {
_balance: 0, // Backing property (by convention, "private")
get balance() {
return this._balance
},
set balance(value) {
if (value < 0) {
throw new Error("Balance cannot be negative")
}
this._balance = value
}
}
account.balance = 100
console.log(account.balance) // 100
account.balance = -50 // Error: Balance cannot be negative
The syntax in classes is identical to object literals:
class Temperature {
constructor(celsius) {
this._celsius = celsius
}
// Getter
get celsius() {
return this._celsius
}
// Setter with validation
set celsius(value) {
if (value < -273.15) {
throw new Error("Temperature below absolute zero!")
}
this._celsius = value
}
// Computed getter — no backing property needed
get fahrenheit() {
return this._celsius * 9/5 + 32
}
// Computed setter — converts and stores
set fahrenheit(value) {
this.celsius = (value - 32) * 5/9 // Uses celsius setter for validation
}
// Read-only getter (no setter)
get kelvin() {
return this._celsius + 273.15
}
}
const temp = new Temperature(25)
console.log(temp.celsius) // 25
console.log(temp.fahrenheit) // 77
console.log(temp.kelvin) // 298.15
temp.fahrenheit = 100
console.log(temp.celsius) // 37.777...
// temp.kelvin = 300 // TypeError in strict mode (no setter)
You can also define getters and setters on the class itself:
class Config {
static _debugMode = false
static get debugMode() {
return this._debugMode
}
static set debugMode(value) {
console.log(`Debug mode ${value ? "enabled" : "disabled"}`)
this._debugMode = value
}
}
console.log(Config.debugMode) // false
Config.debugMode = true // "Debug mode enabled"
console.log(Config.debugMode) // true
You can also define getters and setters using Object.defineProperty(). This creates an accessor descriptor instead of a data descriptor.
const user = {
firstName: "Alice",
lastName: "Smith"
}
Object.defineProperty(user, "fullName", {
get() {
return `${this.firstName} ${this.lastName}`
},
set(value) {
const parts = value.split(" ")
this.firstName = parts[0]
this.lastName = parts[1] || ""
},
enumerable: true,
configurable: true
})
console.log(user.fullName) // "Alice Smith"
user.fullName = "Bob Jones"
console.log(user.firstName) // "Bob"
const obj = {
get prop() { return "value" },
set prop(v) { /* store v */ }
}
const descriptor = Object.getOwnPropertyDescriptor(obj, "prop")
console.log(descriptor)
// {
// get: [Function: get prop],
// set: [Function: set prop],
// enumerable: true,
// configurable: true
// }
// Note: No 'value' or 'writable' — those are for data descriptors
A property descriptor must be either a data descriptor (with value/writable) or an accessor descriptor (with get/set). You cannot mix them.
// ❌ WRONG — mixing data and accessor descriptor
Object.defineProperty({}, "broken", {
value: 42,
get() { return 42 }
})
// TypeError: Invalid property descriptor. Cannot both specify accessors
// and a value or writable attribute
// ❌ ALSO WRONG
Object.defineProperty({}, "alsoBroken", {
writable: true,
set(v) { }
})
// TypeError: Invalid property descriptor.
For more on property descriptors, see Property Descriptors.
Calculate a value from other properties:
const cart = {
items: [
{ name: "Book", price: 20, quantity: 2 },
{ name: "Pen", price: 5, quantity: 10 }
],
get itemCount() {
return this.items.reduce((sum, item) => sum + item.quantity, 0)
},
get subtotal() {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
get tax() {
return this.subtotal * 0.1
},
get total() {
return this.subtotal + this.tax
}
}
console.log(cart.itemCount) // 12
console.log(cart.subtotal) // 90
console.log(cart.tax) // 9
console.log(cart.total) // 99
Enforce constraints when values are assigned:
class User {
constructor(name, age) {
this._name = ""
this._age = 0
// Use setters for initial validation
this.name = name
this.age = age
}
get name() {
return this._name
}
set name(value) {
if (typeof value !== "string" || value.trim() === "") {
throw new Error("Name must be a non-empty string")
}
this._name = value.trim()
}
get age() {
return this._age
}
set age(value) {
if (typeof value !== "number" || value < 0 || value > 150) {
throw new Error("Age must be a number between 0 and 150")
}
this._age = Math.floor(value)
}
}
const user = new User("Alice", 30)
console.log(user.name) // "Alice"
console.log(user.age) // 30
user.age = 31 // Works
user.age = -5 // Error: Age must be a number between 0 and 150
user.name = "" // Error: Name must be a non-empty string
Track property access and changes:
function createTrackedObject(obj, name) {
const tracked = {}
for (const key of Object.keys(obj)) {
let value = obj[key]
Object.defineProperty(tracked, key, {
get() {
console.log(`[${name}] Reading ${key}: ${value}`)
return value
},
set(newValue) {
console.log(`[${name}] Writing ${key}: ${value} → ${newValue}`)
value = newValue
},
enumerable: true
})
}
return tracked
}
const config = createTrackedObject({ debug: false, maxRetries: 3 }, "Config")
config.debug // [Config] Reading debug: false
config.debug = true // [Config] Writing debug: false → true
config.maxRetries // [Config] Reading maxRetries: 3
Defer expensive computation until first access:
const report = {
_data: null,
get data() {
if (this._data === null) {
console.log("Computing expensive data...")
// Simulate expensive computation
this._data = Array.from({ length: 1000 }, (_, i) => i * 2)
}
return this._data
}
}
// Data not computed yet
console.log("Report created")
// First access triggers computation
console.log(report.data.length) // "Computing expensive data..." then 1000
// Second access uses cached value
console.log(report.data.length) // 1000 (no log — already computed)
Trigger updates when values change:
class Observable {
constructor(value) {
this._value = value
this._listeners = []
}
get value() {
return this._value
}
set value(newValue) {
const oldValue = this._value
this._value = newValue
// Notify all listeners
this._listeners.forEach(fn => fn(newValue, oldValue))
}
subscribe(fn) {
this._listeners.push(fn)
return () => {
this._listeners = this._listeners.filter(f => f !== fn)
}
}
}
const count = new Observable(0)
// Subscribe to changes
const unsubscribe = count.subscribe((newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`)
})
count.value = 1 // "Count changed from 0 to 1"
count.value = 2 // "Count changed from 1 to 2"
unsubscribe()
count.value = 3 // (no output — unsubscribed)
The most common mistake is creating a getter or setter that calls itself, causing infinite recursion and a stack overflow.
┌─────────────────────────────────────────────────────────────────────────┐
│ INFINITE RECURSION DISASTER │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ set name(value) { ┌─────────────────────────────┐ │
│ this.name = value ──────►│ Calls the setter again! │ │
│ } │ ▼ │ │
│ ▲ │ set name(value) { │ │
│ │ │ this.name = value ───────┼───┐ │
│ │ │ } │ │ │
│ │ │ ▼ │ │ │
│ │ │ set name(value) { │ │ │
│ │ │ this.name = value ───────┼───┼──┐ │
│ │ │ } │ │ │ │
│ │ │ ▼ │ │ │ │
│ │ │ ... forever until ... │ │ │ │
│ │ │ │ │ │ │
│ │ │ 💥 STACK OVERFLOW! 💥 │ │ │ │
│ │ └─────────────────────────────┘ │ │ │
│ │ │ │ │
│ └───────────────────────────────────────────────────────┘ │ │
│ │ │
└────────────────────────────────────────────────────────────────────┴─────┘
// ❌ WRONG — causes infinite recursion
const user = {
get name() {
return this.name // Calls the getter again!
},
set name(value) {
this.name = value // Calls the setter again!
}
}
user.name = "Alice" // RangeError: Maximum call stack size exceeded
// ✓ CORRECT — use a different property name
const user = {
_name: "", // Backing property
get name() {
return this._name // Reads the backing property
},
set name(value) {
this._name = value // Writes to the backing property
}
}
user.name = "Alice"
console.log(user.name) // "Alice"
// ✓ CORRECT — use private fields
class User {
#name = "" // Private field
get name() {
return this.#name
}
set name(value) {
this.#name = value
}
}
const user = new User()
user.name = "Alice"
console.log(user.name) // "Alice"
// console.log(user.#name) // SyntaxError: Private field
// ✓ CORRECT — use closure
function createUser() {
let name = "" // Closure variable
return {
get name() {
return name
},
set name(value) {
name = value
}
}
}
const user = createUser()
user.name = "Alice"
console.log(user.name) // "Alice"
If you define only a getter without a setter, the property becomes read-only:
"use strict"
const circle = {
radius: 5,
get area() {
return Math.PI * this.radius ** 2
}
// No setter for 'area'
}
console.log(circle.area) // 78.539...
// Attempting to set throws in strict mode
circle.area = 100 // TypeError: Cannot set property area which has only a getter
If you define only a setter without a getter, reading returns undefined:
const logger = {
_logs: [],
set log(message) {
this._logs.push(`[${new Date().toISOString()}] ${message}`)
}
// No getter for 'log'
}
logger.log = "User logged in"
logger.log = "User viewed dashboard"
console.log(logger.log) // undefined — no getter!
console.log(logger._logs) // ["[...] User logged in", "[...] User viewed dashboard"]
Setter-only properties are rare but useful for write-only operations like logging or sending data.
Getters and setters are inherited through the prototype chain, just like regular methods.
const animal = {
_name: "Unknown",
get name() {
return this._name
},
set name(value) {
this._name = value
}
}
// Create object that inherits from animal
const dog = Object.create(animal)
console.log(dog.name) // "Unknown" — inherited getter
dog.name = "Rex" // Uses inherited setter
console.log(dog.name) // "Rex"
// dog has its own _name now
console.log(dog._name) // "Rex"
console.log(animal._name) // "Unknown" — parent unchanged
class Animal {
constructor(name) {
this._name = name
}
get name() {
return this._name
}
set name(value) {
this._name = value
}
}
class Dog extends Animal {
// Override getter to add prefix
get name() {
return `🐕 ${super.name}` // Use super to call parent getter
}
// Override setter to validate
set name(value) {
if (value.length < 2) {
throw new Error("Dog name must be at least 2 characters")
}
super.name = value // Use super to call parent setter
}
}
const dog = new Dog("Rex")
console.log(dog.name) // "🐕 Rex"
dog.name = "Buddy"
console.log(dog.name) // "🐕 Buddy"
dog.name = "X" // Error: Dog name must be at least 2 characters
const parent = {
get value() { return "parent" }
}
const child = Object.create(parent)
// Define own getter
Object.defineProperty(child, "value", {
get() { return "child" },
configurable: true
})
console.log(child.value) // "child"
// Delete child's own getter
delete child.value
console.log(child.value) // "parent" — inherited getter now visible
Unlike regular properties, getters execute their function on every access. MDN documents that getter functions are called each time the property is accessed, which means expensive computations inside getters can become a performance bottleneck if not cached:
let callCount = 0
const obj = {
get expensive() {
callCount++
// Simulate expensive computation
let sum = 0
for (let i = 0; i < 1000000; i++) {
sum += i
}
return sum
}
}
console.log(obj.expensive) // Computes... 499999500000
console.log(obj.expensive) // Computes again!
console.log(obj.expensive) // And again!
console.log(callCount) // 3 — called three times!
For expensive computations, cache the result:
const obj = {
_cachedExpensive: null,
get expensive() {
if (this._cachedExpensive === null) {
console.log("Computing...")
let sum = 0
for (let i = 0; i < 1000000; i++) {
sum += i
}
this._cachedExpensive = sum
}
return this._cachedExpensive
},
invalidateCache() {
this._cachedExpensive = null
}
}
console.log(obj.expensive) // "Computing..." then result
console.log(obj.expensive) // Just result — no computation
console.log(obj.expensive) // Just result — still cached
obj.invalidateCache()
console.log(obj.expensive) // "Computing..." — recalculates
For values that never change, replace the getter with a data property on first access:
const obj = {
get lazyValue() {
console.log("Computing once...")
const value = Math.random() // Expensive computation
// Replace getter with data property
Object.defineProperty(this, "lazyValue", {
value: value,
writable: false,
configurable: false
})
return value
}
}
console.log(obj.lazyValue) // "Computing once..." then 0.123...
console.log(obj.lazyValue) // 0.123... — no log, now a data property
console.log(obj.lazyValue) // 0.123... — same value, no computation
Use regular data properties when:
// ❌ Unnecessary getter
const point = {
_x: 0,
get x() { return this._x }
}
// ✓ Just use a data property
const point = {
x: 0
}
When you call JSON.stringify() on an object, getter values are included in the output (because the getter is called), but setter-only properties result in nothing being included. As the ECMAScript specification defines, JSON.stringify() reads enumerable own properties, which triggers getter functions during serialization:
const user = {
firstName: "Alice",
lastName: "Smith",
get fullName() {
return `${this.firstName} ${this.lastName}`
},
set nickname(value) {
this._nickname = value
}
}
console.log(JSON.stringify(user))
// {"firstName":"Alice","lastName":"Smith","fullName":"Alice Smith"}
// Note:
// - fullName IS included (getter was called)
// - nickname is NOT included (setter-only, no value to serialize)
// - _nickname is NOT included (doesn't exist yet)
Getters and setters are functions that look like properties. Access them without parentheses.
Use get for reading, set for writing. The getter returns a value; the setter receives the assigned value.
Always use a backing property to avoid infinite recursion. Use _name for name, or use private fields (#name).
Getter-only properties are read-only. Assignment fails silently in sloppy mode, throws in strict mode.
Setter-only properties return undefined when read. They're rare but useful for write-only operations.
Accessor descriptors use get/set, not value/writable. You cannot mix them in Object.defineProperty().
Getters execute on every access. Use memoization for expensive computations.
Getters and setters are inherited. Use super.prop to call the parent's accessor in a subclass.
JSON.stringify() calls getters. The computed value is included in the JSON output.
Use getters for computed values, setters for validation. They're perfect for derived properties and enforcing constraints.
</Info>Syntactically, getters are accessed without parentheses, while methods require them:
```javascript
const obj = {
get area() { return 100 },
calculateArea() { return 100 }
}
obj.area // 100 — getter, no parentheses
obj.calculateArea() // 100 — method, with parentheses
obj.calculateArea // [Function] — returns the function itself
```
Semantically, use getters when the value feels like a property (area, fullName, isValid). Use methods when it feels like an action (calculate, fetch, process).
Use a backing property with a different name:
```javascript
// ❌ WRONG — infinite recursion
set name(value) {
this.name = value // Calls setter again!
}
// ✓ CORRECT — use backing property
set name(value) {
this._name = value // Different property
}
```
Alternatively, use private fields (`#name`) or closure variables.
The property becomes read-only:
```javascript
"use strict"
const obj = {
get value() { return 42 }
}
console.log(obj.value) // 42
obj.value = 100 // TypeError: Cannot set property value which has only a getter
```
In non-strict mode, the assignment silently fails instead of throwing.
No. A property descriptor must be either a **data descriptor** (with `value`/`writable`) or an **accessor descriptor** (with `get`/`set`). Mixing them throws a TypeError:
```javascript
Object.defineProperty({}, "prop", {
value: 42,
get() { return 42 }
})
// TypeError: Invalid property descriptor. Cannot both specify
// accessors and a value or writable attribute
```
Use a getter when you need:
1. **Computed values** — derived from other properties
```javascript
get fullName() { return `${this.firstName} ${this.lastName}` }
```
2. **Lazy evaluation** — defer expensive computation
3. **Validation on read** — transform or validate before returning
4. **Encapsulation** — hide the backing storage
Use a regular property when:
- The value doesn't need computation
- No validation is needed
- Performance is critical (getters run on every access)
Getters are called during serialization, and their return values are included in the JSON:
```javascript
const obj = {
a: 1,
get b() { return 2 }
}
JSON.stringify(obj) // '{"a":1,"b":2}'
```
The getter `b` was called, and its value `2` was included. Setter-only properties result in nothing being included (no value to serialize).