fs/promisesとは、Node.jsでファイルやディレクトリを操作する fs モジュールのPromise版APIで、import { readFile } from 'node:fs/promises' と読み込んで await で使います。 コールバックを入れ子にせず、try / catch でエラーを扱えるため、新しく書くコードの第一候補になります。

この記事では、主要APIの早見表とコピペで動くESMコード、存在チェックの正しい書き方、JSONの読み書き、ディレクトリの再帰処理、149MBのログで readFile とストリームのメモリ使用量を実測した比較、同期・コールバック・Promiseの使い分け、エラーコード一覧、path と import.meta.dirname の組み合わせまでを扱います。コードはすべて Node.js v22.22.2(LTS)/ Linux で実行した結果を載せています。

fs/promises 主要APIの早見表

やりたいこと API 主なオプション・戻り値
テキストを読み込む readFile(path, 'utf8') 文字コード省略時は Buffer
ファイルに書き込む(上書き) writeFile(path, data) { flag: 'wx' } で既存なら EEXIST
末尾に追記する appendFile(path, data) ファイルがなければ作成
ディレクトリを作る mkdir(path, { recursive: true }) 途中も作成。既にあってもエラーにならない
一覧を取る readdir(path) 名前の配列
種類つきで一覧を取る readdir(path, { withFileTypes: true }) Dirent(isFile() / isDirectory())
サブフォルダまで一覧 readdir(path, { recursive: true }) 相対パスの配列
サイズ・日時を調べる stat(path) size mtime isFile()
読み書きできるか調べる access(path, constants.R_OK) 失敗時は例外、成功時は undefined
削除する rm(path, { recursive: true, force: true }) ディレクトリごと削除、なくてもエラーなし
名前変更・移動 rename(old, new) 同じファイルシステム内
ファイルをコピー copyFile(src, dest) constants.COPYFILE_EXCL で上書き防止
ディレクトリごとコピー cp(src, dest, { recursive: true }) Node 22 で利用可
変更を監視する watch(path, { signal }) for await でイベントを受け取る
少しずつ一覧を読む opendir(path) for await で1件ずつ
パターンで探す glob('src/**/*.js') v22.0.0で追加。22.22.2では警告なしで動作
大きなファイルを1行ずつ createReadStream + readline node:fs から読み込む
低レベルに開く open(path, flags) FileHandle を返す。readLines() で1行ずつ読める。必ず close()

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Node.js fs/promisesの使い方|ファイル操作APIをコード例で一覧」

主要APIを一度に試すコピペ用コード

以下を api.mjs という名前で保存し、node api.mjs で実行できます。 拡張子を .mjs にするとESM(import 構文)として動きます。.js で使う場合は package.json に "type": "module" を書きます。

// api.mjs(Node.js v22.22.2)
import {
  readFile, writeFile, appendFile, mkdir, readdir, stat, access,
  rm, rename, cp, copyFile, opendir, glob, constants,
} from 'node:fs/promises';

await rm('work', { recursive: true, force: true });
console.log('mkdir:', await mkdir('work/data/2026', { recursive: true }));
console.log('mkdir again:', await mkdir('work/data/2026', { recursive: true }));

await writeFile('work/data/memo.txt', 'line1\n');
await appendFile('work/data/memo.txt', 'line2\n');
console.log('readFile:', JSON.stringify(await readFile('work/data/memo.txt', 'utf8')));
console.log('readFile(Buffer):', await readFile('work/data/memo.txt'));

await writeFile('work/data/2026/a.json', '{}');
const entries = await readdir('work/data', { withFileTypes: true });
console.log('withFileTypes:', entries.map(e => `${e.name}:${e.isDirectory() ? 'dir' : 'file'}`));
console.log('recursive:', await readdir('work', { recursive: true }));

const s = await stat('work/data/memo.txt');
console.log('stat:', s.size, s.isFile(), s.isDirectory(), s.mtime instanceof Date);
console.log('access:', await access('work/data/memo.txt', constants.R_OK | constants.W_OK));

await copyFile('work/data/memo.txt', 'work/data/memo.bak');
await cp('work/data', 'work/backup', { recursive: true });
await rename('work/data/memo.bak', 'work/data/memo.old');
console.log('after cp/rename:', await readdir('work', { recursive: true }));

const dir = await opendir('work/data');
for await (const d of dir) console.log('opendir:', d.name);

const found = [];
for await (const f of glob('work/**/*.json')) found.push(f);
console.log('glob:', found);

await rm('work/backup', { recursive: true });
console.log('after rm:', await readdir('work'));
mkdir: work
mkdir again: undefined
readFile: "line1\nline2\n"
readFile(Buffer): <Buffer 6c 69 6e 65 31 0a 6c 69 6e 65 32 0a>
withFileTypes: [ '2026:dir', 'memo.txt:file' ]
recursive: [ 'data', 'data/2026', 'data/memo.txt', 'data/2026/a.json' ]
stat: 12 true false true
access: undefined
after cp/rename: [
  'backup',
  'data',
  'data/2026',
  'data/memo.old',
  'data/memo.txt',
  'data/2026/a.json',
  'backup/2026',
  'backup/memo.bak',
  'backup/memo.txt',
  'backup/2026/a.json'
]
opendir: memo.old
opendir: 2026
opendir: memo.txt
glob: [ 'work/data/2026/a.json', 'work/backup/2026/a.json' ]
after rm: [ 'data' ]

実行結果から読み取れるポイントです。

結果 わかること
mkdir: work / mkdir again: undefined recursive: true は最初に作ったディレクトリのパスを返し、既にある場合は undefined を返す(エラーにならない)
readFile(Buffer) 文字コードを指定しないと Buffer(バイト列)で返る
access: undefined 成功時は値を返さない。失敗時だけ例外になる
opendir の順番 一覧の順番はファイルシステム任せ。並びを揃えたいときは sort() する

ファイルの存在チェック:access と ENOENT、existsSync の使い分け

読み込むつもりなら、存在チェックをせずに readFile して ENOENT を捕まえるのが基本です。 チェックと読み込みの間にファイルが消える可能性がなく、処理も1回で済みます。

// exists.mjs
import { readFile, access } from 'node:fs/promises';
import { existsSync } from 'node:fs';

// 1. 読み込みたい:読んでみて、なければ null
async function readIfExists(p) {
  try {
    return await readFile(p, 'utf8');
  } catch (e) {
    if (e.code === 'ENOENT') return null;
    throw e;               // 権限エラーなどはそのまま投げる
  }
}

// 2. 有無だけ知りたい:access で判定
async function exists(p) {
  try {
    await access(p);
    return true;
  } catch (e) {
    if (e.code === 'ENOENT') return false;
    throw e;
  }
}

console.log('readIfExists:', await readIfExists('nothing.txt'));
console.log('exists:', await exists('work/data/memo.txt'), await exists('nothing.txt'));
console.log('existsSync:', existsSync('work/data/memo.txt'), existsSync('nothing.txt'));
readIfExists: null
exists: true false
existsSync: true false
方法 戻り方 向いている場面
readFile して ENOENT を捕まえる 中身 or null 読み込むのが目的のとき(推奨)
access(path) 失敗時に例外 非同期処理の中で有無だけ知りたいとき
existsSync(path) true / false 起動時の設定読み込みなど、同期で一度だけ調べるとき
writeFile(path, data, { flag: 'wx' }) 既存なら EEXIST 「なければ作る」を1回で安全に行うとき

fs/promises には exists という関数はありません。existsSync は node:fs から読み込みます。

JSONファイルを読み書きする

JSONは readFile → JSON.parse、JSON.stringify → writeFile の組み合わせで読み書きします。 保存中に処理が止まっても中途半端なファイルが残らないよう、一時ファイルに書いてから rename で置き換える書き方を載せます。

// json.mjs
import { readFile, writeFile, rename } from 'node:fs/promises';
import path from 'node:path';

const settingsPath = path.join(import.meta.dirname, 'work', 'data', 'settings.json');

async function saveJson(file, data) {
  const tmp = file + '.tmp';
  await writeFile(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
  await rename(tmp, file);           // 同じディレクトリ内の rename で置き換え
}

async function loadJson(file) {
  return JSON.parse(await readFile(file, 'utf8'));
}

await saveJson(settingsPath, {
  theme: 'dark',
  items: ['tasks', 'notes'],
  updated: new Date('2026-10-01T09:00:00Z'),
});
const loaded = await loadJson(settingsPath);
console.log(loaded, typeof loaded.updated);

try {
  JSON.parse('{"theme": "dark",}');
} catch (e) {
  console.log(e.name + ':', e.message);
}
{
  theme: 'dark',
  items: [ 'tasks', 'notes' ],
  updated: '2026-10-01T09:00:00.000Z'
} string
SyntaxError: Expected double-quoted property name in JSON at position 17 (line 1 column 18)
  • Date は保存すると文字列になるので、読み込んだ後に new Date(loaded.updated) で戻します。
  • 末尾のカンマなど、JSONとして正しくないと SyntaxError になります。設定ファイルを読むときは try / catch でメッセージを出すと原因がすぐわかります。
  • JSON.stringify(data, null, 2) の 2 はインデント幅です。

ディレクトリを再帰でたどる

サブフォルダまでの一覧なら readdir(dir, { recursive: true }) が最短です。 ファイルだけを絞り込む、途中で処理を止めるなど細かく制御したいときは、withFileTypes と非同期ジェネレーターで自分でたどります。

// walk.mjs
import { readdir } from 'node:fs/promises';
import path from 'node:path';

async function* walk(dir) {
  for (const entry of await readdir(dir, { withFileTypes: true })) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) yield* walk(full);
    else if (entry.isFile()) yield full;
  }
}

for await (const f of walk(path.join(import.meta.dirname, 'work'))) {
  console.log(path.relative(import.meta.dirname, f));
}
work/data/2026/a.json
work/data/memo.old
work/data/memo.txt
work/data/settings.json
方法 結果 向いている場面
readdir(dir, { recursive: true }) 全部の相対パス(ディレクトリも含む) 件数が少なく、まとめて欲しいとき
readdir + withFileTypes の自作ジェネレーター 条件に合うものを1件ずつ node_modules を飛ばす、ファイルだけ欲しいとき
glob('work/**/*.json') パターンに合うパス 拡張子やフォルダ名で絞り込みたいとき

大きなファイルはストリームで:readFile とメモリを実測比較

readFile はファイル全体をメモリに読み込みます。数百MB以上のファイルは createReadStream と readline で1行ずつ処理すると、メモリ使用量をほぼ一定に保てます。

300万行・149,068,890バイトのログ(2026-10-01T09:00:00Z INFO order=0 status=ERROR のような行)から ERROR を含む行を数え、プロセスの最大メモリ使用量(process.resourceUsage().maxRSS)を比べました。

// m_readfile.mjs:全体を読み込む
import { readFile } from 'node:fs/promises';
const t0 = performance.now();
const text = await readFile('big.log', 'utf8');
let errors = 0;
for (const line of text.split('\n')) if (line.includes('ERROR')) errors++;
console.log('readFile  errors=%d %d ms maxRSS=%d MB', errors,
  Math.round(performance.now() - t0), Math.round(process.resourceUsage().maxRSS / 1024));
// m_readline.mjs:1行ずつ読む
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const t0 = performance.now();
const rl = createInterface({ input: createReadStream('big.log', 'utf8'), crlfDelay: Infinity });
let errors = 0;
for await (const line of rl) if (line.includes('ERROR')) errors++;
console.log('readline  errors=%d %d ms maxRSS=%d MB', errors,
  Math.round(performance.now() - t0), Math.round(process.resourceUsage().maxRSS / 1024));
readFile  errors=60000 1103 ms maxRSS=494 MB
readline  errors=60000 1096 ms maxRSS=81 MB
方法 処理時間 最大メモリ(maxRSS)
何もしないNodeプロセス — 41 MB
readFile + split 1,103 ms 494 MB
createReadStream + readline 1,096 ms 81 MB

処理時間はほぼ同じで、メモリは約6分の1になりました。readFile はファイル本体に加えて、文字列と split した配列も同時に持つため、ファイルサイズの3倍以上を使っています。

ヒープ(JavaScriptが使うメモリ領域)の上限を100MBにして実行すると、違いがはっきり出ます。

node --max-old-space-size=100 m_readline.mjs
node --max-old-space-size=100 m_readfile.mjs
readline  errors=60000 1105 ms maxRSS=81 MB
...
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

ストリーム版は上限100MBでも完了し、readFile 版はヒープの上限に達して止まりました。 コンテナなどメモリ上限が決まった環境では、この差が効いてきます。

pipeline でストリームをつなぐ

読み込み → 変換 → 書き込みは stream/promises の pipeline でつなぎます。 エラー時に途中のストリームもまとめて閉じてくれます。

// m_pipeline.mjs
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createGzip } from 'node:zlib';

const t0 = performance.now();
await pipeline(createReadStream('big.log'), createGzip(), createWriteStream('big.log.gz'));
console.log('pipeline gzip %d ms maxRSS=%d MB',
  Math.round(performance.now() - t0), Math.round(process.resourceUsage().maxRSS / 1024));
pipeline gzip 936 ms maxRSS=76 MB

149MBのログが7,874,783バイトのgzipになり、メモリは76MBでした。

ファイルの大きさ・用途 おすすめ
数MB程度の設定・JSON・テンプレート readFile / writeFile(簡潔)
数十MB以上のログ・CSVを1行ずつ処理 createReadStream + readline
変換しながら別ファイルへ書き出す pipeline(読み込み, 変換, 書き込み)
そのままコピー copyFile(OSのコピー機能を使う)

同期・コールバック・Promiseの3方式

Node.jsのファイルAPIには、同じ機能に3つの書き方があります。 新しく書く非同期処理は Promise 版、起動時の短い処理は同期版、が目安です。

// styles.mjs
import fs from 'node:fs';
import fsp from 'node:fs/promises';

const text1 = fs.readFileSync('work/data/memo.txt', 'utf8');            // 同期
fs.readFile('work/data/memo.txt', 'utf8', (err, text2) => {            // コールバック
  if (err) throw err;
  console.log('callback:', JSON.stringify(text2));
});
const text3 = await fsp.readFile('work/data/memo.txt', 'utf8');        // Promise
console.log('sync    :', JSON.stringify(text1));
console.log('promise :', JSON.stringify(text3));

try { fs.readFileSync('nothing.txt'); } catch (e) { console.log('sync error:', e.code); }
fs.readFile('nothing.txt', (err) => console.log('callback error:', err.code));
console.log(fs.promises === fsp);
callback: "line1\nline2\n"
sync    : "line1\nline2\n"
promise : "line1\nline2\n"
sync error: ENOENT
true
callback error: ENOENT

await で待っている間にコールバックが先に実行されたため、callback: が最初に表示されています。コールバック版は完了の順番が書いた順と一致しないことがわかります。また fs.promises と node:fs/promises は同じオブジェクトです。

方式 読み込み元 エラーの受け取り 待っている間 向いている場面
同期(readFileSync) node:fs try / catch 他の処理が止まる CLIツール、起動時の設定読み込み
コールバック(readFile(cb)) node:fs 第1引数の err 他の処理が進む 既存コードとの互換
Promise(await readFile) node:fs/promises try / catch 他の処理が進む 新しく書くコード全般
ストリーム(createReadStream) node:fs pipeline の例外 / error イベント 他の処理が進む 大きなファイル

エラーコード一覧と実際のエラー文

fsのエラーは e.code で判定します。 メッセージ文はバージョンで変わることがあるため、文字列ではなくコードで分岐すると安定します。

// err.mjs
import { readFile, mkdir, writeFile, rmdir, readdir } from 'node:fs/promises';

const tries = {
  ENOENT: () => readFile('nothing.txt', 'utf8'),
  EEXIST: () => mkdir('work/data'),
  EISDIR: () => readFile('work/data'),
  ENOTDIR: () => readdir('work/data/memo.txt'),
  ENOTEMPTY: () => rmdir('work/data'),
  EEXIST_wx: () => writeFile('work/data/memo.txt', 'x', { flag: 'wx' }),
};
for (const [k, fn] of Object.entries(tries)) {
  try { await fn(); } catch (e) { console.log(k, '=>', e.code, '|', e.message); }
}
ENOENT => ENOENT | ENOENT: no such file or directory, open 'nothing.txt'
EEXIST => EEXIST | EEXIST: file already exists, mkdir 'work/data'
EISDIR => EISDIR | EISDIR: illegal operation on a directory, read
ENOTDIR => ENOTDIR | ENOTDIR: not a directory, scandir 'work/data/memo.txt'
ENOTEMPTY => ENOTEMPTY | ENOTEMPTY: directory not empty, rmdir 'work/data'
EEXIST_wx => EEXIST | EEXIST: file already exists, open 'work/data/memo.txt'

EACCES は、権限 600(所有者のみ読み書き可)のファイルを別のユーザーで読んで確認しました。

EACCES => EACCES | EACCES: permission denied, open '/tmp/t110demo/node/work/data/memo.txt'
コード 意味 よくある原因 対処
ENOENT ファイル・ディレクトリがない パスの間違い、基準ディレクトリの違い import.meta.dirname で絶対パスにする
EEXIST 既にある mkdir の重複、flag: 'wx' mkdir は { recursive: true }
EACCES 権限がない 所有者・パーミッション ls -l で確認、書き込み先を見直す
EISDIR ディレクトリに対してファイル操作をした パスの指定ミス stat().isDirectory() で確認
ENOTDIR ファイルをディレクトリとして扱った readdir にファイルを指定 パスを確認
ENOTEMPTY ディレクトリが空でない rmdir で中身ごと消そうとした rm(path, { recursive: true })
EMFILE 開いているファイルが多すぎる FileHandle の閉じ忘れ、大量の同時オープン close() する、同時実行数を絞る

path モジュールと import.meta.dirname

ESMには __dirname がありません。Node.js 20.11以降(22でも利用可)は import.meta.dirname と import.meta.filename で代わりになります。 相対パス 'work/data' は「コマンドを実行した場所」が基準なので、スクリプトの場所を基準にしたいときは import.meta.dirname と path.join を組み合わせます。

// paths.mjs
import path from 'node:path';

console.log('dirname :', import.meta.dirname);
console.log('filename:', import.meta.filename);

const p = '/var/data/report.2026.csv';
console.log(path.basename(p), path.extname(p), path.basename(p, path.extname(p)), path.dirname(p));
console.log(path.join('a', '../b', 'c.txt'), path.resolve('a', 'b'), path.parse(p));
dirname : /tmp/t110demo/node
filename: /tmp/t110demo/node/paths.mjs
report.2026.csv .csv report.2026 /var/data
b/c.txt /tmp/t110demo/node/a/b {
  root: '/',
  dir: '/var/data',
  base: 'report.2026.csv',
  ext: '.csv',
  name: 'report.2026'
}
やりたいこと CommonJS ESM(Node 20.11以降)
スクリプトのディレクトリ __dirname import.meta.dirname
スクリプトのファイルパス __filename import.meta.filename
古いNodeでの代替 — path.dirname(fileURLToPath(import.meta.url))
パスをつなぐ path.join(a, b) 同じ
絶対パスにする path.resolve(a, b) 同じ
ファイル名・拡張子 path.basename(p) / path.extname(p) 同じ

変更を監視する:watch

fs/promises の watch は for await で変更イベントを受け取り、AbortController で止めます。

// watch.mjs
import { watch, writeFile, appendFile } from 'node:fs/promises';

const ac = new AbortController();
setTimeout(async () => {
  await writeFile('work/data/watched.txt', 'a');
  await appendFile('work/data/watched.txt', 'b');
}, 100);
setTimeout(() => ac.abort(), 600);

try {
  for await (const ev of watch('work/data', { signal: ac.signal })) {
    console.log('watch:', ev.eventType, ev.filename);
  }
} catch (e) {
  console.log('stopped:', e.name);
}
watch: rename watched.txt
watch: change watched.txt
watch: change watched.txt
stopped: AbortError

ファイルの新規作成は rename、内容の変更は change として届き、1回の書き込みで複数のイベントが来ることがあります。 保存のたびに処理を走らせるなら、少し待ってからまとめて処理する(デバウンス)と安定します。イベントの出方はOSによって異なります。

よくある質問

Q. fs と fs/promises はどちらを使えばよいですか? A. 新しく書く非同期のコードは node:fs/promises がおすすめです。 existsSync や createReadStream のように fs/promises にないものは node:fs から読み込み、併用して構いません。

Q. ファイルの存在チェックはどう書きますか? A. 読み込むのが目的なら、チェックせず readFile して e.code === 'ENOENT' を判定します。 有無だけ知りたい場合は access()、同期でよければ existsSync() を使います。

Q. ENOENT になりますがファイルはあります。 A. 相対パスはコマンドを実行した場所が基準です。path.join(import.meta.dirname, 'data.json') のようにスクリプトの場所を基準にすると、どこから実行しても同じファイルを指せます。

Q. 大きなファイルを読むとメモリが足りなくなります。 A. createReadStream と readline で1行ずつ処理します。 149MBのログで、readFile は494MB、ストリームは81MBでした。

まとめ

  • import { readFile, writeFile } from 'node:fs/promises' を await で使うのが基本
  • ディレクトリ作成は mkdir(…, { recursive: true })、削除は rm(…, { recursive: true, force: true })
  • 存在チェックは「読んでみて ENOENT」。上書き防止は flag: 'wx'
  • JSONは一時ファイル → rename で置き換えると、途中で止まっても中途半端なファイルが残りにくい
  • 大きなファイルは createReadStream + readline。実測でメモリは494MBから81MBに
  • エラーは e.code で分岐(ENOENT / EEXIST / EACCES / EISDIR / ENOTEMPTY)
  • ESMでは import.meta.dirname と path.join でスクリプト基準のパスを作る

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Node.js fs/promisesの使い方|ファイル操作APIをコード例で一覧」

株式会社RJCでは、Node.jsのファイル操作を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?