Node.jsでファイル操作を行う際、必ず使用するのがfs(File System)モジュールです。しかし、コールバック形式、Promise形式、同期形式と複数のAPIが存在し、どれを使えばよいか迷う方も多いでしょう。
本記事では、Node.js公式が推奨するfs/promisesモジュールを中心に、ファイルの読み書きを非同期で安全に実装する方法を解説します。readFile、writeFile、appendFileの基本的な使い方から、エンコーディングの指定、同期APIとの使い分けまで、実践的なコード例を通じて習得できます。
実行環境#
| 項目 |
バージョン |
| Node.js |
20.x LTS以上 |
| npm |
10.x以上 |
| OS |
Windows/macOS/Linux |
前提条件#
- JavaScriptの基礎知識(関数、オブジェクト、async/await)
- Node.jsの基本操作(スクリプト実行、npm利用)経験
- Promiseの基本的な理解
fsモジュールの3つのAPI形式#
Node.jsのfsモジュールには、3つの異なるAPI形式が用意されています。それぞれの特徴を理解し、適切な場面で使い分けることが重要です。
API形式の比較#
| API形式 |
インポート方法 |
特徴 |
推奨度 |
| Promise API |
node:fs/promises |
async/awaitで記述可能 |
推奨 |
| Callback API |
node:fs |
従来型、ネスト深くなりがち |
互換用 |
| Sync API |
node:fs |
同期処理、イベントループをブロック |
限定的 |
Promise APIを推奨する理由#
Node.js公式ドキュメントでは、新規開発にはfs/promisesの使用を推奨しています。主な理由は以下の通りです。
- 可読性の向上: async/awaitで同期処理のように書ける
- エラーハンドリングの容易さ: try-catchで統一的に処理できる
- コールバック地獄の回避: ネストが深くならない
- モダンなJavaScript: Promiseベースの他のAPIとの親和性が高い
1
2
3
4
5
|
// fs/promisesのインポート(推奨)
import { readFile, writeFile } from 'node:fs/promises';
// 従来のfsモジュール(コールバック・同期API用)
import fs from 'node:fs';
|
node:プレフィックスは、Node.js組み込みモジュールであることを明示的に示します。Node.js 16以降で使用可能で、同名のnpmパッケージとの衝突を防ぐ効果があります。
readFileでファイルを読み込む#
readFileは、ファイルの内容を一括で読み込む関数です。小〜中規模のファイル(数MB程度まで)に適しています。
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import { readFile } from 'node:fs/promises';
async function readTextFile() {
try {
// utf8エンコーディングでテキストとして読み込む
const content = await readFile('./data/message.txt', 'utf8');
console.log('ファイル内容:', content);
return content;
} catch (error) {
if (error.code === 'ENOENT') {
console.error('ファイルが見つかりません:', error.path);
} else if (error.code === 'EACCES') {
console.error('ファイルへのアクセス権限がありません');
} else {
console.error('ファイル読み込みエラー:', error.message);
}
throw error;
}
}
await readTextFile();
|
エンコーディングの指定#
readFileの第2引数でエンコーディングを指定できます。指定方法は文字列またはオプションオブジェクトの2通りがあります。
1
2
3
4
5
6
7
|
import { readFile } from 'node:fs/promises';
// 文字列で指定(シンプル)
const text1 = await readFile('./file.txt', 'utf8');
// オプションオブジェクトで指定(詳細な制御)
const text2 = await readFile('./file.txt', { encoding: 'utf8' });
|
主要なエンコーディング#
| エンコーディング |
用途 |
utf8 / utf-8 |
一般的なテキストファイル(推奨) |
ascii |
ASCII文字のみのファイル |
base64 |
Base64エンコードされたデータ |
hex |
16進数文字列として読み込み |
null(未指定) |
Bufferオブジェクトとして読み込み |
Bufferとして読み込む#
エンコーディングを指定しない場合、readFileはBufferオブジェクトを返します。バイナリファイル(画像、PDF等)を扱う場合や、エンコーディングを後から決定したい場合に便利です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import { readFile } from 'node:fs/promises';
async function readBinaryFile() {
// エンコーディング未指定でBufferとして読み込む
const buffer = await readFile('./image.png');
console.log('データ型:', buffer.constructor.name); // Buffer
console.log('サイズ:', buffer.length, 'bytes');
console.log('先頭8バイト:', buffer.subarray(0, 8));
// 必要に応じて後からテキストに変換可能
// const text = buffer.toString('utf8');
return buffer;
}
await readBinaryFile();
|
JSONファイルの読み込み#
設定ファイルやデータファイルとしてよく使われるJSONファイルの読み込み例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { readFile } from 'node:fs/promises';
async function loadConfig(configPath) {
try {
const jsonText = await readFile(configPath, 'utf8');
const config = JSON.parse(jsonText);
console.log('設定を読み込みました:', config);
return config;
} catch (error) {
if (error.code === 'ENOENT') {
console.log('設定ファイルが見つかりません。デフォルト設定を使用します。');
return { port: 3000, debug: false };
}
if (error instanceof SyntaxError) {
console.error('JSONの形式が不正です:', error.message);
throw error;
}
throw error;
}
}
const config = await loadConfig('./config.json');
|
writeFileでファイルに書き込む#
writeFileは、データをファイルに書き込む関数です。ファイルが存在しない場合は新規作成し、存在する場合は内容を上書きします。
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import { writeFile } from 'node:fs/promises';
async function writeTextFile() {
const content = 'Hello, Node.js!\nThis is a test file.';
try {
await writeFile('./output/message.txt', content, 'utf8');
console.log('ファイルの書き込みが完了しました');
} catch (error) {
if (error.code === 'ENOENT') {
console.error('ディレクトリが存在しません:', error.path);
} else {
console.error('書き込みエラー:', error.message);
}
throw error;
}
}
await writeTextFile();
|
書き込みオプション#
writeFileの第3引数では、詳細な書き込み設定を指定できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import { writeFile } from 'node:fs/promises';
async function writeWithOptions() {
const data = 'Secure data content';
await writeFile('./secure.txt', data, {
encoding: 'utf8',
mode: 0o600, // 所有者のみ読み書き可能
flag: 'wx', // 新規作成のみ(既存ファイルがあればエラー)
flush: true // 書き込み後にディスクへ同期
});
console.log('セキュアファイルを作成しました');
}
|
主要なフラグ(flag)オプション#
| フラグ |
動作 |
w |
書き込み用に開く。ファイルがあれば上書き、なければ作成(デフォルト) |
wx |
書き込み用に開く。ファイルが既に存在すればエラー |
a |
追記用に開く。ファイルがなければ作成 |
ax |
追記用に開く。ファイルが既に存在すればエラー |
JSONファイルの書き込み#
オブジェクトをJSONファイルとして保存する例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import { writeFile } from 'node:fs/promises';
async function saveConfig(config, configPath) {
// 読みやすいフォーマットで出力(インデント2スペース)
const jsonText = JSON.stringify(config, null, 2);
await writeFile(configPath, jsonText, 'utf8');
console.log('設定を保存しました:', configPath);
}
const config = {
server: {
port: 3000,
host: 'localhost'
},
database: {
name: 'myapp',
pool: 10
}
};
await saveConfig(config, './config.json');
|
Bufferの書き込み#
バイナリデータをファイルに書き込む例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import { writeFile } from 'node:fs/promises';
async function writeBinaryData() {
// バイナリデータを作成
const binaryData = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
// Bufferをそのまま書き込み
await writeFile('./binary.dat', binaryData);
console.log('バイナリファイルを作成しました');
}
await writeBinaryData();
|
appendFileでファイルに追記する#
appendFileは、既存ファイルの末尾にデータを追加する関数です。ログファイルの記録など、データを蓄積していく用途に適しています。
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import { appendFile } from 'node:fs/promises';
async function addLogEntry(message) {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}\n`;
await appendFile('./app.log', logEntry, 'utf8');
console.log('ログを追記しました');
}
await addLogEntry('アプリケーションが起動しました');
await addLogEntry('ユーザーがログインしました');
await addLogEntry('データを処理しました');
|
追記時の注意点#
appendFileはファイルが存在しない場合、自動的に新規作成します。この動作を利用して、初回実行時にファイルを作成し、以降は追記していくパターンが実装できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
import { appendFile } from 'node:fs/promises';
class SimpleLogger {
constructor(logPath) {
this.logPath = logPath;
}
async log(level, message) {
const timestamp = new Date().toISOString();
const entry = `${timestamp} [${level.toUpperCase()}] ${message}\n`;
await appendFile(this.logPath, entry, 'utf8');
}
async info(message) {
await this.log('info', message);
}
async error(message) {
await this.log('error', message);
}
async warn(message) {
await this.log('warn', message);
}
}
const logger = new SimpleLogger('./application.log');
await logger.info('サーバーを起動しています...');
await logger.info('ポート3000で待機中');
await logger.warn('メモリ使用率が80%を超えました');
|
同期API(Sync系)の使い方#
同期APIは、処理が完了するまでイベントループをブロックします。サーバーアプリケーションでは避けるべきですが、特定の場面では有用です。
同期APIが適切な場面#
- アプリケーション起動時の設定読み込み: 設定がないと動作できない場合
- CLIツール: ユーザー入力を待つ単純なスクリプト
- ビルドスクリプト: 順序が重要なファイル処理
- テストコード: セットアップ・クリーンアップ処理
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import { readFileSync, writeFileSync, appendFileSync } from 'node:fs';
// 同期的にファイルを読み込む
function loadConfigSync(configPath) {
try {
const content = readFileSync(configPath, 'utf8');
return JSON.parse(content);
} catch (error) {
if (error.code === 'ENOENT') {
console.log('設定ファイルが見つかりません。デフォルト設定を使用します。');
return { port: 3000 };
}
throw error;
}
}
// アプリケーション起動時に同期的に設定を読み込む
const config = loadConfigSync('./config.json');
console.log('設定を読み込みました:', config);
// 同期的にファイルへ書き込む
writeFileSync('./startup.log', 'アプリケーション起動\n', 'utf8');
// 同期的にファイルへ追記する
appendFileSync('./startup.log', `ポート: ${config.port}\n`, 'utf8');
|
同期APIと非同期APIの使い分け#
以下のフローチャートを参考に、どちらのAPIを使用すべきか判断できます。
flowchart TD
A[ファイル操作が必要] --> B{サーバーアプリケーション?}
B -->|Yes| C{起動時の初期化?}
B -->|No| D{CLIツール?}
C -->|Yes| E[同期API可]
C -->|No| F[非同期API推奨]
D -->|Yes| G{処理が順序依存?}
D -->|No| F
G -->|Yes| E
G -->|No| F
E --> H[readFileSync等を使用]
F --> I[fs/promisesを使用]パフォーマンスへの影響#
同期APIがイベントループをブロックする影響を理解しておくことが重要です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import { readFile } from 'node:fs/promises';
import { readFileSync } from 'node:fs';
// 非同期API: 他の処理と並行して実行可能
async function asyncExample() {
console.log('1. 非同期読み込み開始');
const promise = readFile('./large-file.txt', 'utf8');
console.log('2. この行は読み込み中でも実行される');
const content = await promise;
console.log('3. 読み込み完了');
return content;
}
// 同期API: 読み込み完了まで他の処理は実行されない
function syncExample() {
console.log('1. 同期読み込み開始');
const content = readFileSync('./large-file.txt', 'utf8');
// ↑ ここでブロックされる
console.log('2. 読み込み完了後にこの行が実行される');
return content;
}
|
エラーハンドリングのベストプラクティス#
ファイル操作では様々なエラーが発生する可能性があります。適切なエラーハンドリングを実装することで、堅牢なアプリケーションを構築できます。
よくあるエラーコード#
| エラーコード |
意味 |
対処方法 |
ENOENT |
ファイル/ディレクトリが存在しない |
パスの確認、デフォルト値の使用 |
EACCES |
アクセス権限がない |
権限の確認、実行ユーザーの変更 |
EISDIR |
ディレクトリに対してファイル操作を実行 |
パスの確認 |
ENOTDIR |
ファイルに対してディレクトリ操作を実行 |
パスの確認 |
EEXIST |
ファイルが既に存在する(wxフラグ使用時) |
フラグの変更、別名での保存 |
EMFILE |
開いているファイルが多すぎる |
ファイルハンドルの解放 |
構造化されたエラーハンドリング#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
class FileOperationError extends Error {
constructor(operation, path, originalError) {
super(`${operation}に失敗しました: ${path}`);
this.name = 'FileOperationError';
this.operation = operation;
this.path = path;
this.code = originalError.code;
this.originalError = originalError;
}
}
async function safeReadFile(filePath, options = 'utf8') {
try {
return await readFile(filePath, options);
} catch (error) {
throw new FileOperationError('ファイル読み込み', filePath, error);
}
}
async function safeWriteFile(filePath, data, options = 'utf8') {
try {
// ディレクトリが存在しない場合は作成
await mkdir(dirname(filePath), { recursive: true });
await writeFile(filePath, data, options);
} catch (error) {
throw new FileOperationError('ファイル書き込み', filePath, error);
}
}
// 使用例
async function processFile() {
try {
const content = await safeReadFile('./input.txt');
const processed = content.toUpperCase();
await safeWriteFile('./output/result.txt', processed);
console.log('処理が完了しました');
} catch (error) {
if (error instanceof FileOperationError) {
console.error(`${error.operation}エラー`);
console.error(`パス: ${error.path}`);
console.error(`コード: ${error.code}`);
} else {
throw error;
}
}
}
await processFile();
|
実践的なユースケース#
ここまでの知識を組み合わせた、実践的なユースケースを紹介します。
設定ファイルの読み書き#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
class ConfigManager {
constructor(configPath) {
this.configPath = configPath;
this.config = null;
}
async load() {
try {
const content = await readFile(this.configPath, 'utf8');
this.config = JSON.parse(content);
console.log('設定を読み込みました');
} catch (error) {
if (error.code === 'ENOENT') {
console.log('設定ファイルが見つかりません。新規作成します。');
this.config = this.getDefaultConfig();
await this.save();
} else {
throw error;
}
}
return this.config;
}
async save() {
await mkdir(dirname(this.configPath), { recursive: true });
const content = JSON.stringify(this.config, null, 2);
await writeFile(this.configPath, content, 'utf8');
console.log('設定を保存しました');
}
async update(updates) {
this.config = { ...this.config, ...updates };
await this.save();
}
getDefaultConfig() {
return {
appName: 'MyApp',
version: '1.0.0',
settings: {
theme: 'light',
language: 'ja'
}
};
}
}
// 使用例
const configManager = new ConfigManager('./config/app.json');
await configManager.load();
console.log('現在の設定:', configManager.config);
await configManager.update({
settings: {
...configManager.config.settings,
theme: 'dark'
}
});
|
複数ファイルの一括処理#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
import { join, extname, basename } from 'node:path';
async function processAllTextFiles(inputDir, outputDir) {
// 出力ディレクトリを作成
await mkdir(outputDir, { recursive: true });
// ディレクトリ内のファイル一覧を取得
const files = await readdir(inputDir);
const textFiles = files.filter(file => extname(file) === '.txt');
console.log(`${textFiles.length}個のテキストファイルを処理します`);
// 並列で処理
const results = await Promise.all(
textFiles.map(async (file) => {
const inputPath = join(inputDir, file);
const outputPath = join(outputDir, `processed_${file}`);
try {
const content = await readFile(inputPath, 'utf8');
const processed = content
.toUpperCase()
.split('\n')
.map((line, i) => `${i + 1}: ${line}`)
.join('\n');
await writeFile(outputPath, processed, 'utf8');
return { file, success: true };
} catch (error) {
return { file, success: false, error: error.message };
}
})
);
// 結果をレポート
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
console.log(`成功: ${successful.length}件`);
if (failed.length > 0) {
console.log(`失敗: ${failed.length}件`);
failed.forEach(r => console.log(` - ${r.file}: ${r.error}`));
}
}
await processAllTextFiles('./input', './output');
|
まとめ#
Node.jsのfsモジュールを使ったファイル操作について、以下のポイントを解説しました。
- fs/promisesを使用する: async/awaitで可読性の高いコードが書ける
- エンコーディングを意識する: テキストは
utf8、バイナリはBuffer
- エラーハンドリングを適切に行う: エラーコードに応じた処理を実装
- 同期APIは限定的に使用する: 起動時の設定読み込みやCLIツールに限定
次のステップとして、大容量ファイルを効率的に処理するStreamや、ディレクトリ操作(mkdir、readdir、stat)について学ぶことをお勧めします。
参考リンク#