Logo
Published on

Validation

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

AspectSetter ValidationExternal Validation
When ValidatedOn every assignmentWhen explicitly called
Error LocationAt property setLater in process
PerformanceSlight setter overheadBatch validation faster
EncapsulationBuilt into classSeparate concern
Error RecoveryImmediate feedbackDelayed detection
Browser SupportES5+ requiredAll 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.