Menu

Node.js fs/promisesの使い方:async/awaitでファイル操作

fs/promisesはNode.jsのfsモジュールのPromise版です。readFile、writeFile、readdir、stat、mkdir、rm、renameはすべてawaitできるPromiseを返し、エラーはtry/catchで捕まえます。

このページのコードはエディタで実行できます。編集してすぐに結果を確認できます。

fs/promises は、Node.jsの fs モジュールのPromiseベース版です。その関数(readFile、writeFile、readdir、stat、mkdir、rm、rename など)はコールバックを受け取る代わりにPromiseを返すので、await で待ち、エラーは try/catch で捕まえます。require('fs/promises') か import fs from 'fs/promises' でインポートします。

[ 'write docs', 'ship it' ] が表示されます。await は1つの操作が終わるまで次の行に進まないので、追記は必ず最初の書き込みの後に行われます。ESモジュールではトップレベルで await が使えます。CommonJSでは、コードを async 関数で包みましょう。

よく使う関数

名前はコールバックAPIと同じで、コールバックがないだけです。そのうち2つはreadFileとwriteFileのページで詳しく扱っています。

関数動作解決される値
readFile(path, 'utf8')ファイル全体を読む中身
writeFile(path, data)ファイルを作るか置き換えるundefined
appendFile(path, data)末尾に追加するundefined
readdir(path)フォルダの中身を一覧する名前の配列
stat(path)サイズ、日時、ファイルかフォルダかStats オブジェクト
mkdir(path, { recursive: true })フォルダを作る最初に作ったフォルダ、または undefined
rename(from, to)移動または名前変更undefined
rm(path, { recursive: true, force: true })ファイルやフォルダを削除するundefined

readdirとstatでフォルダを一覧する

readdir が返すのは名前だけです。サイズを知りたいときやファイルとフォルダを区別したいときは、それぞれに stat を呼ぶか、{ withFileTypes: true } を渡して、自分の種類を知っているエントリを受け取ります。

ループは README.md 6 bytes、notes.txt 3 bytes、src folder を表示します。stat は存在しないパスに対して ENOENT でrejectするので、詳細も欲しいときは手軽な存在確認になります。

名前変更、移動、削除

rename は(同じドライブ内での)名前変更と移動の両方を行います。rm は削除で、中身のあるフォルダには recursive: true が、パスがすでにないときに失敗させないためには force: true が必要です。

ファイルは新しい名前で drafts から published へ移動し、rm の後の blog フォルダには published だけが残ります。2つ目の rm から force: true を外すと、ENOENT でrejectします。

try/catchでエラーを処理する

失敗した操作はPromiseをrejectします。await を使えば、そのrejectはほかの例外と同じように捕まえられます。何が起きたかはエラーの code でわかります。

1回目の呼び出しはファイルが見つからず [] を返し、2回目は Ada を表示します。覚えておきたいコードは、ENOENT(ファイルがない)、EEXIST(すでにある)、EISDIR(ファイルのはずがフォルダだった)、ENOTEMPTY(フォルダに中身が残っている)、EACCES(権限がない)です。

await も .catch() もしていないPromiseが失敗すると、未処理のrejectになり、Node.jsはエラーで終了します。すべての呼び出しを await するか、それを処理する呼び出し元にPromiseを返しましょう。

Promise.allで複数のファイルを一度に読む

await を使った for ループは、ファイルを1つずつ順に読みます。読み込み同士が依存していないなら、全部を開始して Promise.all で一度だけ待ちましょう。ディスクがそれらを同時に処理します。

Promise.all は入力の配列の順番を保つので、どの読み込みが先に終わっても結果は [ 'alpha', 'beta', 'gamma' ] で、つなげた長さは 14 です。配列の 'b.txt' を 'x.txt' に変えると、await 全体が ENOENT でrejectします。

一部の読み込みが失敗してもすべての結果が欲しいなら、Promise.allSettled を使います。両方ともPromiseのページで扱っています。

よくある間違い

2つのモジュールを取り違える。 require('fs').readFile はコールバックを必要とし、require('fs/promises').readFile はPromiseを返します。コールバック版をコールバックなしで呼ぶと The "cb" argument must be of type function が投げられます。

await を忘れる。 const text = fs.readFile(...) は待機中のPromiseで、text.split は text.split is not a function を投げます。

1つのファイルへの並列の書き込み。 同じパスへの書き込みを複数 Promise.all に入れると、中身が混ざることがあります。そういう書き込みは順番に実行しましょう。

よくある質問

fs.promisesとfs/promisesの違いは何ですか?

違いはありません。require('fs').promises と require('fs/promises') は同じオブジェクトです。今は fs/promises(または node:fs/promises)というパスでインポートするのが一般的です。

fs/promisesにexistsSyncはありますか?

ありません。Promise APIには、パスがないとrejectする access がありますが、よくあるのは必要な操作をそのまま呼んで ENOENT を捕まえる書き方です。ちょっとした確認なら、通常の fs モジュールの existsSync も使えます。

fs/promisesはコールバックAPIより遅いですか?

Promiseのオーバーヘッドの分だけわずかに遅いですが、ディスクにかかる時間に比べれば無視できる差です。読みやすさで選びましょう。新しいコードの多くはPromise APIを使っています。

コールバック型のfs関数をPromiseに変えるには?

ファイル関数は fs/promises がそろえているので、その必要はめったにありません。ほかのコールバックAPIなら、util.promisify(fn) がPromiseを返す版を作ってくれます。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める