pinojs/pino · error · Error

levelVal is read-only

Error message

levelVal is read-only

What it means

The logger prototype defines levelVal only as a getter over an internal symbol; its setter deliberately throws to protect the internal numeric level used for filtering. Assigning logger.levelVal = n is never valid — use setLevel or the level property instead.

Source

Thrown at lib/proto.js:67

  version
} = require('./meta')
const redaction = require('./redaction')

// note: use of class is satirical
// https://github.com/pinojs/pino/pull/433#pullrequestreview-127703127
const constructor = class Pino {}
const prototype = {
  constructor,
  child,
  bindings,
  setBindings,
  flush,
  isLevelEnabled,
  version,
  get level () { return this[getLevelSym]() },
  set level (lvl) { this[setLevelSym](lvl) },
  get levelVal () { return this[levelValSym] },
  set levelVal (n) { throw Error('levelVal is read-only') },
  get msgPrefix () { return this[msgPrefixSym] },
  get [Symbol.toStringTag] () { return 'Pino' },
  [lsCacheSym]: initialLsCache,
  [writeSym]: write,
  [asJsonSym]: asJson,
  [getLevelSym]: getLevel,
  [setLevelSym]: setLevel
}

Object.setPrototypeOf(prototype, EventEmitter.prototype)

// exporting and consuming the prototype object using factory pattern fixes scoping issues with getters when serializing
module.exports = function () {
  return Object.create(prototype)
}

const resetChildingsFormatter = bindings => bindings
function child (bindings, options) {

View on GitHub (pinned to 5aa62305c5)

Solutions

  1. Set the level by name instead: logger.level = 'info' or logger.setLevel('info').
  2. If you need numeric control, define the desired value as a custom level at construction time.
  3. Exclude levelVal when copying/cloning logger properties.
  4. Use logger.levelVal only for reads (e.g. comparisons), never writes.

Example fix

// before
logger.levelVal = 30
// after
logger.level = 'info'
Defensive patterns

Strategy: validation

Validate before calling

if ('levelVal' in changeSet && !('level' in changeSet)) {
  throw new Error('set logger.level by name; levelVal is read-only')
}
Object.assign(logger, changeSet)

Type guard

function isLevelMutationSafe(patch) {
  return !('levelVal' in patch)
}

Try / catch

try {
  applyLoggerPatch(logger, patch)
} catch (e) {
  if (e.message === 'levelVal is read-only') {
    logger.setLevel(patch.levelVal)
  } else throw e
}

Prevention

When it happens

Trigger: logger.levelVal = 30; Object.assign(logger, { levelVal: 30 }); copying properties from one logger object to another including levelVal.

Common situations: Trying to change level 'by value' after reading numeric level APIs; migrating code that manipulated internal fields; bulk-cloning logger state.

Related errors


AI-assisted analysis of pinojs/pino@5aa62305c5 (2026-09-02). Data as JSON: /api/errors/d07c690922be19eb. Report an issue: GitHub.