【Node.js】fsでファイルを読み書きする方法|同期・非同期・Promises

【Node.js】fsでファイルを読み書きする方法|同期・非同期・Promises Node.js

Node.jsでファイルを読み書きするには、標準のfs(File System)モジュールを使います。設定ファイルの読み込み、ログの書き出し、CSVの保存など、サーバーやツールのほとんどで登場します。

fsには3つのAPIがあります。処理を待つ同期コールバック、そしてasync/awaitで書けるPromise版です。さらに、初心者が必ずつまずく罠として、読み込み時にencodingを指定しないと、文字列ではなくBuffer(バイト列)が返るという挙動があります。この記事では、実機のNode.jsで確認しながら、ファイル操作の基本を整理します。

先に結論

  • 読み込みはfs.readFileSync(path, "utf8")が最も簡単です。書き込みはfs.writeFileSync(path, data)です。
  • encodingに"utf8"を指定しないと、文字列ではなくBufferが返ります。テキストなら必ず指定します。
  • APIは3種類:同期(...Sync)、コールバック、Promise(fs/promises)です。
  • サーバーでは同期版が処理を止めるため、fs/promisesawaitを使います。スクリプトなら同期版が手軽です。
  • 上書きはwriteFile、末尾に足すのはappendFileです。
  • 存在確認はexistsSync、フォルダ作成はmkdirSync(path, { recursive: true })です。

requireimportのどちらでfsを読み込むかはrequireとimportの違い、巨大なファイルを扱うならストリームの使い方、JSONの基本はJSONファイルの読み込み方もあわせて参考になります。

スポンサーリンク

fsモジュールの3つのAPI

fsの読み書きには、書き方の異なる3つのAPIがあります。まず全体像をつかみます。

API 書き方 特徴
同期 fs.readFileSync() 結果が戻り値。処理を待つ(ブロックする)
コールバック fs.readFile(path, cb) 従来の非同期。エラーは第1引数
Promise fs/promisesawait 現在の主流。async/awaitで書ける

ファイルを読み込む(同期・コールバック・Promise)

同じ「読み込み」を3つのAPIで書くと、次のようになります。fsモジュールはCommonJSならrequire("fs")、ESモジュールならimportで読み込みます。

read-3ways.js
const fs = require("fs");

// 1. 同期:結果が戻り値で返る(処理を待つ)
const data1 = fs.readFileSync("data.txt", "utf8");
console.log(data1);

// 2. コールバック:第1引数が err、第2引数がデータ
fs.readFile("data.txt", "utf8", (err, data2) => {
    if (err) throw err;
    console.log(data2);
});

// 3. Promise:async 関数の中で await する
const fsp = require("fs/promises");

async function main() {
    const data3 = await fsp.readFile("data.txt", "utf8");
    console.log(data3);
}
main();

実機でも、3つとも同じ内容を読み込めました。コールバック版は第1引数が必ずエラー(無ければnull)という決まりがあり、これを確認してからデータを使います。Promise版はfs/promises(またはfs.promises)を使い、awaitで結果を受け取れるため、いちばん読みやすく書けます。

【最重要】encodingを指定しないとBufferが返る

もっともつまずきやすいのがこれです。readFileSyncの第2引数に"utf8"を指定しないと、戻り値は文字列ではなくBuffer(バイト列)になります。

buffer-gotcha.js
const fs = require("fs");

// NG: encoding を指定しないと Buffer が返る
const buf = fs.readFileSync("data.txt");
console.log(buf);              // <Buffer e3 81 93 ...>(バイト列)
console.log(buf.constructor.name);   // Buffer

// OK: "utf8" を指定すると文字列が返る
const text = fs.readFileSync("data.txt", "utf8");
console.log(text);            // こんにちは(文字列)
console.log(typeof text);     // string

// Buffer を後から文字列にするなら toString
console.log(buf.toString("utf8"));   // こんにちは
テキストを読むときは “utf8” を忘れない

実機で確認したところ、encodingを指定しないreadFileSync("data.txt")はBufferを返し、先頭バイトは227(UTF-8の1バイト目)でした。"utf8"を指定すると、ちゃんと文字列"こんにちは"が返ります。「文字列として処理したいのに、なぜか<Buffer ...>と表示される」「.split()が思った通りに動かない」といったときは、encodingの指定漏れを疑ってください。すでにBufferを受け取ってしまった場合は、buf.toString("utf8")で文字列に変換できます。

ファイルに書き込む・追記する

書き込みはwriteFile系、末尾に足すのはappendFile系です。writeFile既存の内容を上書きするので注意してください。

write-append.js
const fs = require("fs");

// 上書きで書き込む(ファイルが無ければ作成)
fs.writeFileSync("log.txt", "1行目\n");

// 末尾に追記する(既存の内容は消えない)
fs.appendFileSync("log.txt", "2行目\n");
fs.appendFileSync("log.txt", "3行目\n");

console.log(fs.readFileSync("log.txt", "utf8"));
// 1行目
// 2行目
// 3行目

// Promise 版も同じ
const fsp = require("fs/promises");
async function save() {
    await fsp.writeFile("out.txt", "保存する内容");
}
save();

実機でも、writeFileSyncで作ったファイルにappendFileSyncを2回行うと3行になりました。ログのように追記したいのにwriteFileを使うと、毎回上書きされて前の内容が消えてしまいます。「追記したいのか、上書きしたいのか」で使い分けてください。

どのAPIを使うべきか(同期 vs 非同期)

3つのAPIは、使う場面で選びます。判断の軸は「処理を止めても良いか」です。

  • 同期(...Sync:書きやすく分かりやすい。処理が終わるまで他が止まるため、起動時の設定読み込みや、小さなスクリプトに向きます。
  • Promise(fs/promisesawaitで書け、処理を止めません。Webサーバーなど、同時に多くの処理をこなす場面では必ずこちらを使います。
  • コールバック:従来の方式。今から書くならPromise版のほうが読みやすくおすすめです。

サーバーの中でreadFileSyncを使うと、その読み込みが終わるまで他のリクエストもすべて待たされます。サーバーではfs/promisesawaitを基本にしてください。同期版は、起動時に一度だけ読む設定ファイルや、使い捨てのスクリプトに限定するのが安全です。

存在確認・フォルダ作成・一覧

ファイル操作では、読み書き以外もよく使います。存在確認・フォルダ作成・一覧取得の基本です。

exists-mkdir-readdir.js
const fs = require("fs");

// 存在確認
if (fs.existsSync("data.txt")) {
    console.log("ファイルがあります");
}

// フォルダ作成(recursive で親フォルダごと作る)
fs.mkdirSync("logs/2026/03", { recursive: true });

// フォルダ内の一覧
const names = fs.readdirSync(".");
console.log(names);   // ["data.txt", "logs", ...]

// ファイルの削除
// fs.unlinkSync("old.txt");

実機でも、mkdirSync("a/b/c", { recursive: true })で親フォルダごと作成でき、readdirSyncで中身の一覧が取れました。recursive: trueを付けないと、途中のフォルダが無いときにエラーになります。すでにフォルダがある場合もrecursive: trueならエラーになりません。

JSONを読み書きする

設定ファイルなどでJSONを扱うときは、fsで文字列として読み書きし、JSON.parseJSON.stringifyで変換します。

json-fs.js
const fs = require("fs");

// 読み込み:文字列で読んで JSON.parse でオブジェクトにする
const text = fs.readFileSync("config.json", "utf8");
const config = JSON.parse(text);
console.log(config.name);

// 書き込み:JSON.stringify で文字列にして書く
const data = { name: "テスト", count: 3 };
fs.writeFileSync(
    "out.json",
    JSON.stringify(data, null, 2)   // null, 2 で見やすく整形
);

実機でも、JSON.parse(fs.readFileSync(..., "utf8"))でJSONをオブジェクトとして読み込めました。JSON.stringify(data, null, 2)2は、インデント幅の指定で、人が読みやすい整形済みJSONになります。CommonJSならrequire("./config.json")でも読めますが、requireは内容をキャッシュするため、実行中に書き換わるファイルにはfsを使います。

エラー処理

ファイルが無い、権限が無いといった失敗に備えます。エラーの受け取り方はAPIごとに違います。

error-handling.js
const fs = require("fs");

// 同期:try-catch で受ける
try {
    const data = fs.readFileSync("nothere.txt", "utf8");
} catch (err) {
    console.error("読み込み失敗:", err.code);   // ENOENT など
}

// Promise:try-catch(async 関数内)
const fsp = require("fs/promises");
async function main() {
    try {
        await fsp.readFile("nothere.txt", "utf8");
    } catch (err) {
        console.error("読み込み失敗:", err.code);
    }
}

// コールバック:第1引数 err を確認する
fs.readFile("nothere.txt", "utf8", (err, data) => {
    if (err) {
        console.error("読み込み失敗:", err.code);
        return;
    }
    console.log(data);
});

実機でも、存在しないファイルを読むと、同期版もPromise版もerr.codeENOENT(ファイルが見つからない)になりました。よくあるエラーコードは、ファイルが無いENOENT、権限が無いEACCESです。err.codeで分岐すると、原因に応じた処理を書けます。

よくある失敗

encodingを指定せずBufferが返って混乱する

テキストとして読むならreadFileSync(path, "utf8")のように必ずencodingを指定します。指定しないとBufferになります。

追記したいのにwriteFileで上書きする

writeFileは既存の内容を上書きします。末尾に足したいならappendFileを使います。

サーバーで同期版(readFileSync)を使う

同期版は処理が終わるまで他をすべて止めます。Webサーバーではfs/promisesawaitを使ってください。

mkdirで親フォルダが無くてエラーになる

階層ごと作るならmkdirSync(path, { recursive: true })を使います。すでにある場合もエラーになりません。

エラー処理を書かずに落ちる

ファイルが無い・権限が無いは普通に起きます。同期はtry-catch、コールバックは第1引数のerrを必ず確認します。

よくある質問

QreadFileSyncの結果がBufferになるのはなぜですか?
Aencodingを指定していないためです。fs.readFileSync(path, "utf8")のように"utf8"を指定すると文字列が返ります。すでにBufferを受け取った場合はbuf.toString("utf8")で文字列に変換できます。
Q同期版とPromise版はどちらを使うべきですか?
AWebサーバーなど同時に多くの処理をこなす場面では、処理を止めないfs/promisesawaitを使います。起動時の設定読み込みや使い捨てのスクリプトなら、書きやすい同期版(readFileSync)で十分です。
Qファイルに追記するには?
Afs.appendFileSync(path, data)を使います。writeFileは既存の内容を上書きしてしまうため、ログのように足していきたいときはappendFileを使ってください。
Qフォルダごと作るには?
Afs.mkdirSync("a/b/c", { recursive: true })のようにrecursive: trueを指定します。途中のフォルダが無くても親ごと作成し、すでにある場合もエラーになりません。
QJSONファイルを読み込むには?
AJSON.parse(fs.readFileSync("config.json", "utf8"))で読み込めます。書き込みはfs.writeFileSync(path, JSON.stringify(data, null, 2))です。実行中に変わるファイルは、キャッシュされるrequireではなくfsで読みます。

まとめ

  • 読み込みはreadFileSync(path, "utf8")、書き込みはwriteFileSync(path, data)が基本です。
  • encodingに"utf8"を指定しないとBufferが返ります。テキストでは必ず指定します。
  • APIは同期・コールバック・Promiseの3種類。今から書くならfs/promisesawaitがおすすめです。
  • サーバーでは同期版を避け、fs/promisesを使います。
  • 上書きはwriteFile、追記はappendFile。フォルダ作成はmkdirrecursive: trueです。
  • エラーは同期ならtry-catch、コールバックなら第1引数のerrで受けます。

Node.jsのファイル操作は、3つのAPIの使い分けと、encodingの指定さえ押さえれば難しくありません。普段はfs/promisesawaitを基本に、起動時のちょっとした読み込みは同期版で、と使い分けると快適に書けます。