Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to use WeakMap for private data in JavaScript

WeakMap provides truly private data storage for objects without memory leaks, as keys are weakly referenced and garbage collected when no other references exist. As the creator of CoreUI with 26 years of JavaScript development experience, I’ve used WeakMap to implement private properties in components and services that handle sensitive data for millions of users.

The most secure approach uses WeakMap instances to store private state associated with public object instances.

Basic Private Data Pattern

const privateData = new WeakMap()

class User {
  constructor(name, password) {
    // Public property
    this.name = name

    // Private data stored in WeakMap
    privateData.set(this, {
      password: password,
      loginCount: 0
    })
  }

  login(password) {
    const data = privateData.get(this)

    if (data.password === password) {
      data.loginCount++
      return true
    }
    return false
  }

  getLoginCount() {
    return privateData.get(this).loginCount
  }
}

const user = new User('john', 'secret123')
console.log(user.name) // 'john'
console.log(user.password) // undefined (private)
console.log(user.login('secret123')) // true
console.log(user.getLoginCount()) // 1

Multiple Private Properties

const _password = new WeakMap()
const _email = new WeakMap()
const _createdAt = new WeakMap()

class Account {
  constructor(username, email, password) {
    this.username = username

    _email.set(this, email)
    _password.set(this, password)
    _createdAt.set(this, new Date())
  }

  authenticate(password) {
    return _password.get(this) === password
  }

  getEmail() {
    return _email.get(this)
  }

  updateEmail(newEmail, password) {
    if (this.authenticate(password)) {
      _email.set(this, newEmail)
      return true
    }
    return false
  }

  getAccountAge() {
    const created = _createdAt.get(this)
    return Math.floor((Date.now() - created) / (1000 * 60 * 60 * 24))
  }
}

const account = new Account('john', '[email protected]', 'pass123')
console.log(account.getEmail()) // '[email protected]'
console.log(account.updateEmail('[email protected]', 'wrong')) // false
console.log(account.updateEmail('[email protected]', 'pass123')) // true

Private Methods Pattern

const privateMethods = new WeakMap()

class Calculator {
  constructor() {
    privateMethods.set(this, {
      validateNumber(n) {
        if (typeof n !== 'number' || isNaN(n)) {
          throw new Error('Invalid number')
        }
      },

      formatResult(result) {
        return Math.round(result * 100) / 100
      }
    })
  }

  add(a, b) {
    const methods = privateMethods.get(this)
    methods.validateNumber(a)
    methods.validateNumber(b)
    return methods.formatResult(a + b)
  }

  divide(a, b) {
    const methods = privateMethods.get(this)
    methods.validateNumber(a)
    methods.validateNumber(b)

    if (b === 0) {
      throw new Error('Division by zero')
    }

    return methods.formatResult(a / b)
  }
}

const calc = new Calculator()
console.log(calc.add(10, 5)) // 15
console.log(calc.divide(10, 3)) // 3.33

Singleton with Private State

const instances = new WeakMap()

class Database {
  constructor() {
    // Singleton pattern
    if (instances.has(Database)) {
      return instances.get(Database)
    }

    instances.set(Database, this)

    instances.set(this, {
      connected: false,
      queries: []
    })
  }

  connect() {
    const state = instances.get(this)
    state.connected = true
    console.log('Database connected')
  }

  query(sql) {
    const state = instances.get(this)

    if (!state.connected) {
      throw new Error('Not connected')
    }

    state.queries.push({
      sql,
      timestamp: Date.now()
    })

    return { success: true }
  }

  getQueryCount() {
    return instances.get(this).queries.length
  }
}

const db1 = new Database()
const db2 = new Database()

console.log(db1 === db2) // true (singleton)
db1.connect()
db1.query('SELECT * FROM users')
console.log(db2.getQueryCount()) // 1

Event Emitter with Private Listeners

const listeners = new WeakMap()

class EventEmitter {
  constructor() {
    listeners.set(this, new Map())
  }

  on(event, callback) {
    const eventListeners = listeners.get(this)

    if (!eventListeners.has(event)) {
      eventListeners.set(event, [])
    }

    eventListeners.get(event).push(callback)
  }

  emit(event, data) {
    const eventListeners = listeners.get(this)
    const callbacks = eventListeners.get(event)

    if (callbacks) {
      callbacks.forEach(callback => callback(data))
    }
  }

  off(event, callback) {
    const eventListeners = listeners.get(this)
    const callbacks = eventListeners.get(event)

    if (callbacks) {
      const index = callbacks.indexOf(callback)
      if (index > -1) {
        callbacks.splice(index, 1)
      }
    }
  }

  getListenerCount(event) {
    const eventListeners = listeners.get(this)
    const callbacks = eventListeners.get(event)
    return callbacks ? callbacks.length : 0
  }
}

const emitter = new EventEmitter()
const handler = (data) => console.log('Received:', data)

emitter.on('message', handler)
emitter.emit('message', 'Hello')
console.log(emitter.getListenerCount('message')) // 1

React Component with Private State

const privateState = new WeakMap()

class SecureInput {
  constructor(element) {
    this.element = element

    privateState.set(this, {
      value: '',
      masked: true,
      history: []
    })

    this.element.addEventListener('input', (e) => {
      this.updateValue(e.target.value)
    })
  }

  updateValue(newValue) {
    const state = privateState.get(this)
    state.history.push(state.value)
    state.value = newValue

    this.render()
  }

  toggleMask() {
    const state = privateState.get(this)
    state.masked = !state.masked
    this.render()
  }

  render() {
    const state = privateState.get(this)

    if (state.masked) {
      this.element.value = '*'.repeat(state.value.length)
    } else {
      this.element.value = state.value
    }
  }

  getValue() {
    return privateState.get(this).value
  }

  undo() {
    const state = privateState.get(this)

    if (state.history.length > 0) {
      state.value = state.history.pop()
      this.render()
    }
  }
}

// Usage
const input = new SecureInput(document.getElementById('password'))
input.toggleMask()
console.log(input.getValue()) // Access real value

Best Practice Note

This is the same WeakMap pattern we use in CoreUI’s components for truly private data encapsulation. WeakMap provides memory-safe private storage because entries are garbage collected when the key object is no longer referenced. Always use WeakMap instead of closures or Symbols when you need private instance data that should be collected with the instance.

For related encapsulation patterns, check out how to use WeakSet in JavaScript and how to implement module pattern in JavaScript.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
How to loop inside React JSX
How to loop inside React JSX

How to Clone an Object in JavaScript
How to Clone an Object in JavaScript

The Best Bootstrap Alternative for Developers in 2025
The Best Bootstrap Alternative for Developers in 2025

How to migrate CoreUI React Templates to Vite
How to migrate CoreUI React Templates to Vite

Answers by CoreUI Core Team