pinojs/pino · error · Error

only one of option.transport or stream can be specified

Error message

only one of option.transport or stream can be specified

What it means

Pino throws this when both a transport option and a stream argument are passed to pino(). A transport spawns a worker thread that builds its own destination stream, so a user-supplied stream would be contradictory. Only one output mechanism may be specified.

Source

Thrown at lib/tools.js:331

      stream.end()
    })
  } else {
    // For some reason istanbul is not detecting this, but it's there
    /* istanbul ignore next */
    // We do not have an event loop, so flush synchronously
    stream.flushSync()
  }
}

function createArgsNormalizer (defaultOptions) {
  return function normalizeArgs (instance, caller, opts = {}, stream) {
    // support stream as a string
    if (typeof opts === 'string') {
      stream = buildSafeSonicBoom({ dest: opts })
      opts = {}
    } else if (typeof stream === 'string') {
      if (opts && opts.transport) {
        throw Error('only one of option.transport or stream can be specified')
      }
      stream = buildSafeSonicBoom({ dest: stream })
    } else if (opts instanceof SonicBoom || opts.writable || opts._writableState) {
      stream = opts
      opts = {}
    } else if (opts.transport) {
      if (opts.transport instanceof SonicBoom || opts.transport.writable || opts.transport._writableState) {
        throw Error('option.transport do not allow stream, please pass to option directly. e.g. pino(transport)')
      }
      if (opts.transport.targets && opts.transport.targets.length && opts.formatters && typeof opts.formatters.level === 'function') {
        throw Error('option.transport.targets do not allow custom level formatters')
      }

      let customLevels
      if (opts.customLevels) {
        customLevels = opts.useOnlyCustomLevels ? opts.customLevels : Object.assign({}, opts.levels, opts.customLevels)
      }
      stream = transport({ caller, ...opts.transport, levels: customLevels })

View on GitHub (pinned to 5aa62305c5)

Solutions

  1. Remove the second stream argument and configure the destination inside the transport options (e.g. target 'pino/file' with its options.destination).
  2. Or drop opts.transport and keep passing the stream/destination string directly: pino('/path/to/log').
  3. If using targets, put everything in transport.targets and pass no stream.
  4. If the stream is required, wrap it with pino.destination and pass it as the sole stream argument instead of a transport.

Example fix

// before
const logger = pino({ transport: { target: 'pino/file', options: { destination: './app.log' } } }, './app.log')
// after
const logger = pino({ transport: { target: 'pino/file', options: { destination: './app.log' } } })
Defensive patterns

Strategy: validation

Validate before calling

function validatePinoArgs(opts, stream) {
  if (opts && opts.transport && stream != null) {
    throw new TypeError('Pass either opts.transport OR a stream, not both')
  }
  return true
}

Type guard

const isStreamLike = (s) => s && (typeof s === 'object') && (typeof s.writable === 'boolean' || '_writableState' in s)

Try / catch

try {
  const logger = pino(opts, stream)
} catch (err) {
  if (err.message.includes('only one of option.transport or stream')) {
    loggerFallback = pino(opts.transport ? opts : stream)
  } else throw err
}

Prevention

When it happens

Trigger: Calling pino({ transport: { target: 'pino/file' } }, someStream) — i.e. opts.transport is truthy while the second argument is a string path or stream.

Common situations: Copying examples that use transport into code that already passes a destination string/file stream; migrating from pino(destination) to the newer transport API without removing the second argument; a wrapper function merging both config styles.

Related errors


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