Fix: unable to determine transport target for pino-pretty
Reproduce the exact failure in a clean container, then pick the right fix: skip the transport outside production, install pino-pretty as a real dependency, use it as a stream, or wire up a bundler plugin.
TL;DR#
pino({ transport: { target: "pino-pretty" } }) throws unable to determine transport target for "pino-pretty" when pino cannot resolve the pino-pretty module from the files that called
pino(). The two reproducible causes are: pino-pretty is not installed at all, or it is
installed only as a devDependency and a production-only install never installed it. The fix
that avoids it entirely is a transport conditional on NODE_ENV, kept a devDependency, with
NODE_ENV=production genuinely set at runtime; otherwise install pino-pretty as a real
dependency, use it as a stream, or wire up a bundler plugin.
The error#
/app/node_modules/pino/lib/transport.js:295
throw new Error(`unable to determine transport target for "${origin}"`)
^
Error: unable to determine transport target for "pino-pretty"
at fixTarget (/app/node_modules/pino/lib/transport.js:295:13)
at transport (/app/node_modules/pino/lib/transport.js:261:30)
at normalizeArgs (/app/node_modules/pino/lib/tools.js:349:16)
at pino (/app/node_modules/pino/pino.js:92:28)This is reported on Stack Overflow with the code that triggers it most often — a logger.js
that configures a transport target directly: unable to determine transport target for
"pino-pretty".
import pino from "pino";
const logger = pino({
transport: {
target: "pino-pretty",
options: { colorize: true },
},
});
export default logger;Why it happens#
Since pino v7, a transport
target such as "pino-pretty" is not loaded in the thread that
calls pino(). It is loaded in a separate worker thread, and the worker needs an absolute
module path, not a bare package name. To turn "pino-pretty" into that absolute path, pino's
fixTarget() function walks the files that called pino.transport() and tries
createRequire(callerFile).resolve("pino-pretty") against each one, stopping at the first that
succeeds. Simplified from the current source:
for (const filePath of callers) {
try {
fixTarget = createRequire(filePath).resolve(origin)
break
} catch (err) {
continue
}
}
if (!fixTarget) {
throw new Error(`unable to determine transport target for "${origin}"`)
}(adapted from lib/transport.js, pinojs/pino;
the real function also resolves targets/pipeline arrays and bundler overrides). If every
caller fails to resolve pino-pretty, fixTarget stays unset and pino throws exactly this
error.
Two situations reproduce that failure directly:
- pino-pretty is not installed.
npm install pinowas run,pino-prettynever was, and nothing innode_modulesanswers to that name. - pino-pretty is installed, but only as a
devDependency, and whatever runs the process does not installdevDependencies.npm ci --omit=devis a production-only install: it never puts anything listed underdevDependenciesintonode_modules. Some platforms get there a different way — Heroku's own Node.js Classic Buildpack Builds page states that the buildpack installsdevDependenciesfor the build, then "prunes out the packages declared under devDependencies before deploying the application", so a devDependency the running process needs is gone by the time it starts (the same mechanism breaks ababel-nodestart script, see Fix babel-node: not found on Heroku (devDependencies pruned)). A Docker build that skipsdevDependencieshits the same gap for the TypeScript compiler itself, see Fix: sh: tsc: not found in Docker build. Either way, the package that printed colourised logs locally is simply absent where the process actually runs, and the exact same "unable to determine transport target" error reappears — this time in production, not innpm run dev.
A third situation is named directly by pino's own documentation, without this article being
able to reproduce it in a plain container: when the app is bundled into one file (a single
compiled server entry point from webpack or esbuild). Pino's docs on
bundling state it plainly: "due to
its internal architecture based on Worker Threads, it is not possible to bundle Pino without
generating additional files" — a bundler has to emit pino-pretty, pino/file, pino-worker
and thread-stream-worker as separate files and register them on
globalThis.__bundlerPathsOverrides. Pino's docs do not say which exact lookup step fails
without that. Next.js carries the same risk, but its default serverExternalPackages list in
both the current stable release (v16.4.2) and the canary branch already names pino,
pino-pretty and pino-roll — pino-pretty's own entry there is commented 'A "plugin" that is
passed to pino as a string (and is then required in a worker)' — so an app on a current Next.js
version is less likely to hit this from bundling specifically. One reply in the same Stack
Overflow thread, on a question opened in 2022, reports hitting this error after deploying an
app built with next-pino, and that adding import 'pino-pretty' to the logger file made it
go away — reported as a workaround by that answer, not explained by any source checked for this
article.
Fix#
1. Make the transport conditional, and keep pino-pretty as a devDependency#
Pino's own docs on pretty printing
say NDJSON "is perfect for production usage and long-term storage" and that it's "not so great
for development environments", pointing to a prettifier such as pino-pretty for those. A
Stack Overflow answer on this question (score 9) agrees that pino-pretty "should be a dev
dependency". Following that, the fix is to only build the transport outside production, so
pino-pretty never has to exist in the production node_modules at all — provided
NODE_ENV=production is genuinely set wherever the process runs, since the ternary below falls
back to the pino-pretty branch for any other value, including an unset one:
const pino = require("pino");
const logger = pino({
transport:
process.env.NODE_ENV === "production"
? undefined
: { target: "pino-pretty", options: { colorize: true } },
});Keep pino-pretty under devDependencies — no change there. Reproduced on Node 20.20.2
(node:20-alpine), pino 10.4.0, pino-pretty 13.2.0, 16 October 2026: with pino-pretty only in
devDependencies and a fresh npm ci --omit=dev (so it is genuinely absent from
node_modules), running with NODE_ENV=production prints plain NDJSON with no error at all —
transport resolves to undefined and pino never calls fixTarget(). Running the same file
with NODE_ENV=development (after reinstalling pino-pretty) prints the pretty-formatted line
instead. As a negative control, the same absent-pino-pretty setup with NODE_ENV left unset
throws the exact same transport error as before — the conditional only helps once the process
that runs it actually sets NODE_ENV=production (a Dockerfile ENV NODE_ENV=production, or
whatever the deploy platform sets).
2. Need pretty output in production too — move pino-pretty to a real dependency#
npm uninstall pino-pretty
npm install --save pino-prettyIf pino-pretty is already listed under devDependencies, running npm install pino-pretty
on its own is not enough: reproduced on the same setup, it leaves the package under
devDependencies unchanged (npm keeps an existing dependency's current type). Uninstalling it
first and reinstalling with --save moves it to dependencies. If the production process is
meant to keep printing pretty logs, it has to be
there, because a production-only install never brings in devDependencies. Reproduced the
same way: with pino-pretty only in devDependencies, npm ci --omit=dev leaves it out of
node_modules and the transport error returns, even though the same code had just logged fine
after a plain npm install. Moving the package to dependencies and re-running the exact same
npm ci --omit=dev installs it and the logger works — nothing else in the logger configuration
changes.
A different-looking but related failure is a platform-specific optional dependency going
missing after node_modules was built for another OS — not this error, but the same family of
"the module isn't actually in node_modules for this run": see Fix Cannot find module
@rollup/rollup-win32-x64-msvc.
3. Skip transport resolution entirely — use pino-pretty as a stream#
If you only need pretty output in one process and do not want to depend on pino's
caller-stack resolution at all, require pino-pretty directly and pass it to pino() as a
destination stream instead of a transport.target:
const pino = require("pino");
const pretty = require("pino-pretty");
const stream = pretty({ colorize: true });
const logger = pino({ level: "info" }, stream);
module.exports = logger;pino's own docs say only the transport code runs in a worker thread (see "Since pino v7" above);
loading pino-pretty with a plain require() like this stays outside that worker, so there is
no module target for pino to resolve. Reproduced on the same Node 20.20.2 / pino 10.4.0 /
pino-pretty 13.2.0 setup: with pino-pretty installed, this logs normally. With it uninstalled,
require("pino-pretty") fails as a plain Node module load, not as pino's transport error:
Error: Cannot find module 'pino-pretty'
Require stack:
- /app/logger-stream.js— a more standard MODULE_NOT_FOUND that most Node developers already know how to read. The
stream usage itself is documented under "Usage as a stream" in pino-pretty's
README.
4. Bundled into one file (webpack, esbuild, or an older Next.js) — add a bundler plugin#
If pino-pretty is installed and the error only shows up once the app is bundled or deployed
as a single compiled entry point, pino's documentation names two routes, neither reproduced
here: a bundler plugin — pino-webpack-plugin
for Webpack, or esbuild-plugin-pino for
esbuild — that generates the extra transport files and registers them for you, or setting
globalThis.__bundlerPathsOverrides by hand with the paths your bundler actually emitted for
pino-worker, pino/file, thread-stream-worker and pino-pretty (the docs list all four as
required identifiers). On Next.js specifically, check first whether the installed version's
default serverExternalPackages list already names pino/pino-pretty before adding either
of the above. Reaching for fix 3 above (stream mode) also sidesteps this case, since the worker
thread and its target resolution are never involved — at the cost of running pino-pretty's
formatting outside that worker instead of inside it.
Verify the fix#
For fix 1, run with NODE_ENV=production against a node_modules built by
npm ci --omit=dev (so pino-pretty is genuinely absent) and confirm a plain NDJSON line
prints with no error:
{"level":30,"time":1792175093067,"pid":75,"hostname":"b815110f6b71","msg":"hello from conditional transport"}Then run with NODE_ENV=development (with pino-pretty installed) and confirm the
pretty-formatted line prints instead. For fix 2, run the process the same way it runs in the
environment that failed — a fresh rm -rf node_modules && npm ci --omit=dev, not just
npm install on a machine that already has node_modules cached — and confirm a coloured,
formatted log line prints instead of the stack trace:
[18:19:39.836] INFO (305): hello from reproIf the error persists after either fix and pino-pretty is confirmed present in
node_modules, the next case to rule out is bundling: check whether the process that threw the
error is a single compiled bundle rather than plain node_modules-based code.
Pino's worker-thread transport design buys speed — formatting and writing logs off the main thread — at the cost of needing a resolvable module for every transport target, in whatever environment actually runs the process. Once that trade-off is visible, the reproduced fixes above cover the common reports of this error: skip the transport outside production, install the package where the running process will actually look for it, or skip the resolution step entirely by using a stream.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix Docker npm EBADENGINE: Unsupported engine
Read the Required and Actual lines, distinguish npm self-upgrades from application installs, and stop a floating npm release breaking an unchanged Dockerfile.
Fix npm WARN package.json: No repository field
The warning is informational and harmless. Silence it for good in an application, fill it in properly for a library, and know why modern npm no longer prints it.
Fix esbuild spawnSync ETXTBSY in Docker: Node 20.3 io_uring
Why npm install of esbuild started failing with ETXTBSY on node:20.3 images in June 2023, what changed in libuv and Docker since, and the build fix that holds today.


