How Validation Improves Code Quality
Understanding validation in setters enables developers to enforce business rules and data constraints at the property level. This technique prevents invalid state from ever entering objects while providing clear error messages, making it essential for modern JavaScript development. Teams using setter validation report fewer runtime errors and more robust data handling.
TL;DR
- Use
set
to validate on assignment- Validation works seamlessly with type checking
- Reduces invalid state bugs
- Perfect for enforcing business rules and constraints
const result = process(data)
The Validation Challenge
You're debugging a user profile system where invalid data is causing crashes throughout the application. Ages are negative, emails are malformed, and usernames contain illegal characters. Without validation at the property level, bad data spreads before detection.
// The problematic no-validation approach
class User {
constructor(name, email, age) {
this.name = name // No validation!
this.email = email // Could be invalid!
this.age = age // Could be negative!
}
}
const user = new User('', 'not-an-email', -25)
console.log('Invalid user:', user.age) // -25!
Modern setter validation prevents invalid data from entering objects with immediate feedback:
// The elegant solution with setter validation
class User {
set name(val) {
if (!val || val.length < 2) throw new Error('Invalid')
this._name = val
}
set email(val) {
if (!/^[^@]+@[^@]+\.[^@]+$/.test(val)) throw new Error('Bad')
this._email = val
}
set age(val) {
if (val < 0 || val > 150) throw new Error('Range')
this._age = val
}
}
console.log('Setter validation prevents bad data')
Best Practises
Use validation when:
- ✅ Properties have constraints or business rules
- ✅ Preventing invalid state is critical
- ✅ User input needs sanitization and checking
- ✅ Creating domain models with invariants
Avoid when:
- 🚩 Simple data transfer objects without rules
- 🚩 Performance-critical property updates
- 🚩 Validation logic is complex or async
- 🚩 External validation libraries are preferred
System Design Trade-offs
Aspect | Setter Validation | External Validation |
---|---|---|
When Validated | On every assignment | When explicitly called |
Error Location | At property set | Later in process |
Performance | Slight setter overhead | Batch validation faster |
Encapsulation | Built into class | Separate concern |
Error Recovery | Immediate feedback | Delayed detection |
Browser Support | ES5+ required | All browsers |
More Code Examples
❌ Validation chaos
// External validation - easy to bypass
class Product {
constructor(name, price, stock) {
this.name = name
this.price = price
this.stock = stock
}
// Separate validation method
validate() {
const errors = []
if (!this.name || this.name.length < 3) {
errors.push('Name too short')
}
if (this.price < 0) {
errors.push('Price cannot be negative')
}
if (this.stock < 0) {
errors.push('Stock cannot be negative')
}
return errors
}
}
const product = new Product('AB', -10, -5)
// Object created with invalid data!
console.log('Invalid product exists:', product)
// Must remember to validate
const errors = product.validate()
console.log('Errors found:', errors)
// Direct property access bypasses validation
product.price = -999
product.stock = -100
console.log('More invalid data:', product.price)
// Validation only when explicitly called
if (product.validate().length > 0) {
console.log('Product invalid after changes')
}
// Easy to forget validation
function processProduct(p) {
// Forgot to validate!
return p.price * p.stock // Negative result!
}
console.log('Processing invalid:', processProduct(product))
✅ Built-in validation
// Built-in setter validation
class Product {
constructor() {
this._name = ''
this._price = 0
this._stock = 0
}
get name() {
return this._name
}
set name(val) {
if (!val || val.length < 3) {
throw new Error('Name must be at least 3 characters')
}
this._name = val.trim()
}
get price() {
return this._price
}
set price(val) {
if (typeof val !== 'number' || val < 0) {
throw new Error('Price must be non-negative number')
}
this._price = Math.round(val * 100) / 100
}
get stock() {
return this._stock
}
set stock(val) {
if (!Number.isInteger(val) || val < 0) {
throw new Error('Stock must be non-negative integer')
}
this._stock = val
}
get value() {
return this.price * this.stock
}
}
const product = new Product()
// Invalid data rejected immediately
try {
product.name = 'AB' // Too short!
} catch (e) {
console.log('Caught:', e.message)
}
console.log('Example complete')
Technical Trivia
The Validation Bug of 2021: A banking app allowed negative account balances because validation was only in the UI layer. Attackers bypassed the frontend and directly called APIs with negative transfer amounts, creating money from nothing by exploiting the missing backend validation.
Why the pattern failed: The Account class stored balance as a plain property without setter validation. While the UI validated inputs, direct API calls could set account.balance = -1000000, and the system processed negative balances as credits. The missing property-level validation allowed impossible states.
Modern tooling prevents these issues: Today's setter validation ensures business rules are enforced at the data model level. TypeScript decorators like @Min and @Max provide compile-time validation hints. Frameworks like class-validator integrate validation directly into class properties.
Master Validation: Implementation Strategy
Implement setter validation when properties have invariants that must always hold. The immediate feedback and inability to bypass validation outweigh the slight performance overhead. Combine with TypeScript for compile-time checks and consider validation libraries for complex rules. Remember that setter validation is your last line of defense against invalid state.