pinojs/pino · error · Error

customLevels is required if useOnlyCustomLevels is set true

Error message

customLevels is required if useOnlyCustomLevels is set true

What it means

useOnlyCustomLevels: true tells Pino to recognize ONLY the levels defined in customLevels, discarding default levels. Without customLevels there would be zero levels, so Pino throws at logger creation.

Solutions

  1. Add a customLevels object, e.g. customLevels: { foo: 10, bar: 20 }.
  2. Or remove useOnlyCustomLevels if you want default levels merged with custom ones.
  3. Verify the option spelling: customLevels (plural), defined as { name: number } with numbers > 0.

Example fix

// before
const logger = pino({ useOnlyCustomLevels: true })
// after
const logger = pino({ useOnlyCustomLevels: true, customLevels: { trace2: 15, debug2: 25 } })
Defensive patterns

Strategy: validation

Validate before calling

function validateCustomLevels(opts) {
  if (opts && opts.useOnlyCustomLevels && !(opts.customLevels && Object.keys(opts.customLevels).length > 0)) {
    throw new TypeError('useOnlyCustomLevels: true requires a non-empty customLevels map')
  }
  return true
}

Type guard

const hasCustomLevels = (opts) => Boolean(opts && opts.customLevels && typeof opts.customLevels === 'object' && Object.keys(opts.customLevels).length > 0)

Try / catch

try {
  const logger = pino(opts)
} catch (err) {
  if (err.message.startsWith('customLevels is required if useOnlyCustomLevels')) {
    logger = pino({ ...opts, customLevels: { info: 30, error: 50 } })
  } else throw err
}

Prevention

When it happens

Trigger: Calling pino({ useOnlyCustomLevels: true }) or pino({ level: 'mylevel', useOnlyCustomLevels: true }) without a customLevels map.

Common situations: Copy-pasting custom-level examples and enabling useOnlyCustomLevels before defining customLevels; enabling the flag in shared config where customLevels is conditionally provided; typos like customLevel (singular).

Understand the failure class

Background: "Must pass :limit option" / "Missing required option" — required option errors explained — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at pino.js:164

    [stringifySafeSym]: stringifySafe,
    [formattersSym]: allFormatters
  })

  let chindings = ''
  if (base !== null) {
    if (name === undefined) {
      chindings = coreChindings(base)
    } else {
      chindings = coreChindings(Object.assign({}, base, { name }))
    }
  }

  const time = (timestamp instanceof Function)
    ? timestamp
    : (timestamp ? epochTime : nullTime)
  const timeSliceIndex = time().indexOf(':') + 1

  if (useOnlyCustomLevels && !customLevels) throw Error('customLevels is required if useOnlyCustomLevels is set true')
  if (mixin && typeof mixin !== 'function') throw Error(`Unknown mixin type "${typeof mixin}" - expected "function"`)
  if (msgPrefix && typeof msgPrefix !== 'string') throw Error(`Unknown msgPrefix type "${typeof msgPrefix}" - expected "string"`)

  assertDefaultLevelFound(level, customLevels, useOnlyCustomLevels)
  const levels = mappings(customLevels, useOnlyCustomLevels)

  if (stream && stream[transportUsesMultistreamSym] === true) {
    let sampleLabel = typeof level === 'string' ? level : undefined
    if (!sampleLabel || levels[sampleLabel] === undefined) {
      sampleLabel = Object.keys(levels)[0]
    }
    const sampleNumber = levels[sampleLabel]
    let ok = false
    try {
      const formatted = formatters.level(sampleLabel, sampleNumber)
      ok = formatted && typeof formatted === 'object' && formatted.level === sampleNumber
    } catch {
      ok = false

View on GitHub (pinned to 5aa62305c5)