pinojs/pino · error · Error

exported worker is not a function

Error message

exported worker is not a function

What it means

When a transport is loaded, Pino imports the transport module and unwraps its exported builder function, tolerating up to two nested .default exports from transpilation (ESM/TS interop). If after unwrapping the export is still not a function, the transport module's export shape is invalid.

Source

Thrown at lib/transport-stream.js:53

      // When bundled with pkg, an undefined error is thrown when called with realImport
      // When bundled with pkg and using node v20, an ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING error is thrown when called with realImport
      // More info at: https://github.com/pinojs/thread-stream/issues/143
      try {
        fn = realRequire(decodeURIComponent(target))
      } catch {
        throw error
      }
    } else {
      throw error
    }
  }

  // Depending on how the default export is performed, and on how the code is
  // transpiled, we may find cases of two nested "default" objects.
  // See https://github.com/pinojs/pino/issues/1243#issuecomment-982774762
  if (typeof fn === 'object') fn = fn.default
  if (typeof fn === 'object') fn = fn.default
  if (typeof fn !== 'function') throw Error('exported worker is not a function')

  return fn
}

View on GitHub (pinned to 5aa62305c5)

Solutions

  1. Export a function from the transport module: module.exports = async function (opts) { ... } or export default async function (opts) { ... }.
  2. If the export is an object, add a callable default export.
  3. Check the resolved path in transport.target points at the transport file, not an index that re-exports an object.
  4. If transpiled output has >2 nested defaults, restructure the export so the function is at default or module.exports directly.
  5. Test with node -e "const m=require('./my-transport.js'); console.log(typeof (m.default||m))" to verify it resolves to 'function'.

Example fix

// before (my-transport.js)
export default { build: async (opts) => buildStream }
// after
export default async function (opts) {
  return buildStream
}
Defensive patterns

Strategy: validation

Validate before calling

function validateTransportModule(target) {
  const mod = require(require.resolve(target))
  let fn = mod
  if (typeof fn === 'object') fn = fn.default
  if (typeof fn === 'object') fn = fn.default
  if (typeof fn !== 'function') {
    throw new TypeError(`Transport ${target} must export a function, got ${typeof fn}`)
  }
  return true
}

Type guard

const isTransportFn = (mod) => {
  let fn = mod
  if (typeof fn === 'object') fn = fn.default
  if (typeof fn === 'object') fn = fn.default
  return typeof fn === 'function'
}

Try / catch

try {
  const logger = pino({ transport: { target } })
} catch (err) {
  if (err.message === 'exported worker is not a function') {
    console.error(`Fix export in ${target}: default-export or module.exports a function`)
  }
  throw err
}

Prevention

When it happens

Trigger: Passing transport.target pointing at a module whose export is an object, class instance, or arrow-assigned object without a callable default; e.g. module.exports = { myTransport } without .default, or a CommonJS module exporting a non-function.

Common situations: Custom TypeScript transports compiled with esModuleInterop producing nested default objects deeper than Pino's double-unwrap; writing a transport as a plain object instead of a function; wrong file path resolving to a barrel file exporting an object; forgetting module.exports = fn in CJS.

Related errors


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