Node だけで色付きのログを出せる util.styleText を使おうとすると、次のエラーで止まることがあります。
SyntaxError: The requested module 'node:util' does not provide an export named 'styleText'
at ModuleJob._instantiate (node:internal/modules/esm/module_job:123:21)
node:util は Node に組み込みのモジュールなので、インストール漏れでもパスの打ち間違いでもありません。実行している Node が古く、その版の node:util に styleText がまだ無い、それだけです。
やっかいなのは、境界がメジャーバージョンではなくマイナーバージョンにあることです。「Node 20 だから使えるはず」では判定できません。
まず node -v をマイナーまで読む
styleText が入った版は次のとおりです。
| Node | import { styleText } from 'node:util' |
|---|---|
| v18.20.8 | SyntaxError |
| v20.11.1 | SyntaxError |
| v20.12.0 | 通る |
| v21.6.2 | SyntaxError |
| v21.7.0 | 通る |
| v22 / v24 | 通る |
境界は 20.12.0 と 21.7.0 です。Node 18 には最後まで入りませんでした。
node -v が v20.11.1 や v18.x を返したなら、原因はこれで確定します。手元では通るのに CI やコンテナで落ちるときも、たいていは同じ理由で、そちらの Node だけが 20.12.0 より古いという形です。
なお、v21.6.2 は v20.12.0 より版番号が大きいのに styleText を持ちません。この API は先に開発版の 21 系(21.7.0)へ入り、そのあと LTS の 20 系(20.12.0)へ取り込まれたためです。同じ 20.12.0 / 21.7.0 で入った API は他にもあり、crypto.hash がそうです。この「版番号の逆転」そのものについては TypeError: crypto.hash is not a function の直し方 で詳しく扱っているので、そちらを参照してください。
styleText の場合、Node を上げて import が通ったあとに、まだ 2 回止まります。
直し方:Node を 22 以降に上げる
コードは 1 文字も変えません。同じソースのまま、実行する Node を上げれば通ります。
上げ先は 22 以降にしてください。20.12.0 で止めないでください。 理由は 3 つあり、順に後述します(保守期限・配列フォーマット・CI ログの色)。まず手順です。
手元の Node(nvm):
nvm install 22
nvm use 22
node -v # v22.x が出れば入れ替わっている
Docker:
# Before
FROM node:20.11-alpine
# After
FROM node:22-alpine
GitHub Actions:
- uses: actions/setup-node@v4
with:
node-version: '22' # '20.11' のようなピン留めが原因になっていることがある
node-version: 20 のように系だけを書いた場合、入るのは 20 系の最終版です。これは 20.12.0 より新しいので、import は通ります。20 系はすでに保守終了していて、この指定を放置してもこれ以上は上がりません。
だから、このエラーが出ているなら、20.11 のようにマイナーまで固定しているか、ベースイメージや OS のパッケージが古い 20.x を配っているかのどちらかです。手元と CI で node -v の出力を並べると、どちらが古いかがすぐ分かります。
ただし、import が通ることと styleText が期待どおり動くことは別です。20 系は最後の版まで上げても、配列で指定した色だけは非 TTY でも消えません(後述)。上げ先を 22 以降にしてほしいのは、そのためでもあります。
「styleText が使える版」は 1 つに決まらない
styleText の版境界は 1 本ではなく、3 本あります。しかも別々の版に散らばっています。ここが「Node 20.12 以上が必要」という一行の答えでは足りない理由です。
| 何が変わるか | 20 系 | 22 系 |
|---|---|---|
import { styleText } が通る | 20.12.0 から | 最初から |
配列フォーマット styleText(['bold','green'], s) が通る | 20.13.0 から | 最初から |
| 非 TTY(パイプ・CI ログ)で色が抑制される | 20.18.0 から(文字列指定のみ。配列指定は 20 系では最後まで抑制されない) | 22.8.0 から(文字列指定)/22.15.0 から(配列指定も) |
そのうえで、Node 20 は 2026-04-30 に保守期間が終了しています(Node 公式のリリーススケジュール)。いま 20.12.0 へ上げても、サポートの切れた系列の中で 1 つマイナーを進めるだけで、下の 2 つの落とし穴も残ったままです。22(2027-04-30 まで)か 24(2028-04-30 まで)へ移ってください。
上げたのに、今度は TypeError で落ちる
Node を 20.12.0 に上げた読者が次に踏むのがこれです。
import { styleText } from 'node:util';
console.log(styleText(['bold', 'green'], 'PASS'));
TypeError: The argument 'format' must be one of: 'reset', 'bold', 'dim', ... Received [ 'bold', 'green' ]
複数のスタイルを配列でまとめて渡す書き方は、styleText が入った 20.12.0 の時点では使えません。 20 系では 20.13.0 から通るようになりました(22 系は最初から通ります)。ネット上のサンプルは配列指定で書かれていることが多いので、「styleText は存在するのに書き方が通らない」という形でもう一度止まります。
エラーの種類が SyntaxError から TypeError に変わるため、同じ原因(Node が古い)だとは見えません。20.12.0 だけを目標にして上げると、ここでもう一往復することになります。
色が出ない、あるいは CI ログにエスケープが残る
styleText は途中から、出力先が端末でないときは色を付けないよう変わりました。パイプ・リダイレクト・CI のログはどれも端末ではないので、この変更がそのまま見え方の差になります。
- 20.17.0 まで/22.7.0 まで:端末でなくてもエスケープシーケンスを出します。
node app.mjs > out.logとすると、ログに制御文字が残ります。 - 20.18.0 から/22.8.0 から:端末でなければ素のテキストになります。「手元では色が付くのに CI のログでは付かない」のはこの変更によるもので、故障ではありません。
ただし、この抑制が効くのは文字列指定のときだけです。配列指定は 20 系の最後まで抑制されません。
次の表は**すべてパイプ先(=端末ではない)**での挙動です。NO_COLOR の行は、端末で実行した場合に色が消えるかどうかを表します。
| 実行条件 | v20.12.0 | v20.20.2(20 系の最後) | v22.23.1 / v24.18.0 |
|---|---|---|---|
| 文字列指定・パイプ先 | 色が出る | 抑制される | 抑制される |
文字列指定・端末で NO_COLOR=1 | 抑制されない | 抑制される | 抑制される |
| 配列指定・パイプ先 | TypeError | 抑制されない | 抑制される |
配列指定・端末で NO_COLOR=1 | TypeError | 抑制されない | 抑制される |
つまり Node 20 系には、配列指定で付いた色を止める手段がありません。 端末でなくても、NO_COLOR=1 / NODE_DISABLE_COLORS=1 / FORCE_COLOR=0 のどれを立てても、エスケープシーケンスが出ます(20 系の最後の版で 4 通りとも確認しました)。20 系はすでに保守が終わっているので、これが最終形です。「文字列指定の行は CI ログがきれいなのに、配列指定にした行だけ制御文字が混ざる」という食い違いは、これで説明が付きます。
逆に CI でも色付きのログを見たい場合は、第 3 引数で端末の検査を切れます。
styleText('green', 'PASS', { validateStream: false });
これは 20.18.0 / 22.8.0 以降で意図どおりに働きます(それ以前はもともと抑制されないので、指定してもしなくても色が出ます)。なお { validateStream: true } は既定値なので、渡しても結果は変わりません。
書き方で変わるエラー文字列
同じ「Node が古い」でも、styleText の取り出し方によってメッセージが変わります。SyntaxError で検索して見つからないときは、自分が見ているのが下のどれかを確かめてください。
| 書き方 | 古い Node で出るもの | いつ落ちるか |
|---|---|---|
import { styleText } from 'node:util' | SyntaxError: The requested module 'node:util' does not provide an export named 'styleText' | 実行前(モジュールの解決時) |
const { styleText } = require('node:util') | TypeError: styleText is not a function | 呼び出した行 |
import util from 'node:util' → util.styleText(...) | TypeError: util.styleText is not a function | 呼び出した行 |
ESM の名前付き import だけが、実行前に落ちます。 import の解決は本体の評価より前に行われるので、styleText を呼ぶ行に一度も到達しなくても、そのファイルは読み込まれた時点で SyntaxError になります。console.log を仕込んでも何も出ないのはこのためで、コードのどこが悪いかを探しても見つかりません。
一方 require とデフォルト import は、モジュールの取得までは成功します。styleText が undefined のまま関数として呼ばれ、その行で TypeError になります。
切り分け
node -vは 20.12.0 以上なのにSyntaxErrorが出る → そのnodeと、実際にスクリプトを走らせているnodeが別物である可能性があります。process.execPathをログに出すと、どのバイナリで動いているかが分かります(node -e "console.log(process.execPath, process.version)")。エディタの実行ボタン、npm run経由、Docker の中、CI のジョブでそれぞれ違う Node が使われていることがあります。node:utilではなくutilと書いている →import { styleText } from 'util'でも同じエラーになります。原因も対処も同じで、接頭辞の有無は関係ありません。does not provide an export namedは出るが、名前がstyleTextではない → 別の API の話です。ただし読み方は同じで、その API が入った Node の版を調べ、実行している Node と比べるという手順は変わりません。- TypeScript で型エラーになる → 実行時とは別の話です。
@types/nodeが古いと、Node 側にstyleTextがあっても型定義に無いため、コンパイル時に落ちます。@types/nodeを上げてください。 ERR_UNKNOWN_BUILTIN_MODULEやCannot find module 'node:util'→ これはnode:接頭辞そのものを解釈できない、さらに古い Node(14 以前)です。styleText以前の問題なので、Node の入れ替えが先になります。
検証環境
node:20.11.1-bookworm、ネットワーク有り。同じイメージに nodejs.org から Node 20.12.0 の公式ビルドを展開し、2 つの Node を同居させています- 再現:
node reproduce/app.mjs(イメージの Node 20.11.1)でdoes not provide an export named 'styleText'が出て終了コード 1 - 修正:同一のソースを展開した Node 20.12.0 で実行し、終了コード 0・シグネチャ消滅。
reproduce/app.mjsとfix/app.mjsに差分はありません(変えたのは実行する Node だけです) - 本文の 3 つの表(追加の版・配列フォーマット・非 TTY での抑制)は、v18.20.8 / v20.11.1 / v20.12.0 / v20.13.0 / v20.17.0 / v20.18.0 / v20.20.2 / v21.6.2 / v21.7.0 / v22.0.0 / v22.7.0 / v22.8.0 / v22.14.0 / v22.15.0 / v22.17.0 / v22.18.0 / v22.23.1 / v23.11.1 / v24.18.0 の公式イメージで個別に実行して確認しました(出力はすべてパイプ経由=非 TTY)。配列指定の抑制が 22 系のどの版で入ったかは、22.14.0(抑制されない)と 22.15.0(抑制される)の間まで絞り込みました
- Node 各系列の保守期限は、Node 公式のリリーススケジュールによります
nvm / Volta / actions/setup-node / Dockerfile での実際の入れ替え手順は、機構が同じ(実行する Node の版が境界)ことを根拠にした対処であり、それぞれの経路での再現は通していません。