Node.jsでの開発において、ファイルやディレクトリの存在を確認する処理は非常に頻繁に発生する基本的なタスクです。
しかし、Node.jsには同期・非同期を含め複数の手法が存在するため、プロジェクトの規模や用途に応じて最適な方法を選択する必要があります。
特にパフォーマンスや安全性を重視する場合、単に「存在するかどうか」を確認するだけでは不十分なケースも少なくありません。
この記事では、現代のNode.js開発において推奨されるファイル存在確認の手法と、それぞれの使い分けについて詳しく解説します。
現代のNode.jsにおけるファイル存在確認の考え方
Node.jsの進化に伴い、ファイル操作を扱うfsモジュールも大きく変化してきました。
かつてはコールバック形式が主流でしたが、現在はPromiseベースのAPIを利用するのが標準的なアプローチとなっています。
ファイル存在確認には主にfs.existsSync、fs.promises.access、fs.promises.statの3つの手法が用いられます。
これらの中でどれを選択するかは、プログラムが実行されるタイミングや、スケーラビリティへの要求によって決まります。
基本的には、「メインの処理を止めない非同期処理」を優先的に検討することが、高速なサーバーサイドアプリケーションを構築する鍵となります。
同期メソッド fs.existsSync の特性と活用シーン
fs.existsSyncは、指定したパスが存在するかどうかを真偽値(boolean)で返す同期型のメソッドです。
非同期処理の記述が不要なため、コードが非常にシンプルで読みやすいというメリットがあります。
しかし、同期処理であるため、実行中はNode.jsのイベントループをブロックしてしまう点に注意が必要です。
このため、高頻度でリクエストを処理するサーバー側のロジック内での使用は推奨されません。
一方で、プログラムの起動時に設定ファイルを読み込む場合や、一回限りのスクリプト作成においては、そのシンプルさが大きな武器となります。
fs.existsSync の実装例
const fs = require('fs');
const path = './config.json';
// 同期的にファイル存在確認を行う
if (fs.existsSync(path)) {
console.log('ファイルが見つかりました。');
} else {
console.log('ファイルは存在しません。');
}
ファイルが見つかりました。
このように、条件分岐の中で直感的に使用できるのがfs.existsSyncの最大の特徴です。
複雑なエラーハンドリングが不要な場面では、依然として便利な選択肢の一つと言えるでしょう。
非同期メソッド fs.promises.access によるモダンな確認方法
非同期環境でファイル存在確認を行う際の標準的な手法は、fs.promises.accessを利用することです。
このメソッドは、指定したファイルに対するアクセス権限を確認するためのものですが、存在確認のみを行うことも可能です。
fs.promisesを使用することで、async/await構文を利用した読みやすい非同期コードを記述できます。
イベントループをブロックしないため、高負荷なアプリケーションでもパフォーマンスを損なうことがありません。
ただし、ファイルが存在しない場合は例外(Error)をスローするため、try...catch構文でのハンドリングが必須となります。
fs.promises.access の実装例
const fs = require('fs').promises;
async function checkFile(path) {
try {
// F_OK定数を渡すことで存在確認のみを行う
await fs.access(path, fs.constants.F_OK);
console.log('ファイルは存在し、アクセス可能です。');
} catch (error) {
console.log('ファイルが存在しないか、アクセス権限がありません。');
}
}
checkFile('./data.txt');
ファイルは存在し、アクセス可能です。
fs.constants.F_OKは、ファイルが可視である(存在している)ことを確認するためのフラグです。
他にもR_OK(読み取り可能か)やW_OK(書き込み可能か)といった定数を組み合わせることで、より詳細なチェックが可能です。
fs.promises.stat を使った詳細情報の取得
ファイルの存在確認だけでなく、そのファイルが「ディレクトリなのかファイルなのか」を知りたい場合は、fs.promises.statが適しています。
statメソッドは、ファイルのサイズ、作成日時、パーミッションなどの詳細なメタデータを取得します。
存在しないパスを指定した場合はaccessと同様にエラーが発生するため、適切にキャッチする必要があります。
単なる存在確認だけであればaccessの方が軽量ですが、ファイル種別を判定したい場合にはこのメソッドが必須となります。
fs.promises.stat の実装例
const fs = require('fs').promises;
async function getFileInfo(path) {
try {
const stats = await fs.stat(path);
if (stats.isFile()) {
console.log('これはファイルです。');
} else if (stats.isDirectory()) {
console.log('これはディレクトリです。');
}
} catch (error) {
if (error.code === 'ENOENT') {
console.log('対象が存在しません。');
}
}
}
getFileInfo('./uploads');
これはディレクトリです。
エラーオブジェクトのcodeプロパティを確認し、ENOENT(Error No Entity)であれば「存在しない」と判断するのが一般的なパターンです。
なぜ「存在確認してから開く」のが推奨されないのか
ファイル操作において、多くの開発者が陥りやすい罠が「存在を確認してから操作する」というロジックです。
例えば、「ファイルが存在することを確認し、その後に中身を読み込む」という二段階の処理が挙げられます。
実は、Node.js(および一般的なファイルシステム操作)において、このパターンは推奨されていません。
その理由は、TOCTOU(Time of Check to Time of Use)と呼ばれる競合状態(レースコンディション)が発生する可能性があるからです。
存在確認を行った直後、別のプロセスによってファイルが削除される可能性があるため、確認が無意味になるケースがあるのです。
そのため、最も堅牢な方法は「最初からファイルを開きにいき、エラーが起きたら対処する」という設計です。
推奨されるエラーハンドリングパターン
const fs = require('fs').promises;
async function readFileSafely(path) {
try {
// 存在確認をせずに直接読み込みを試みる
const content = await fs.readFile(path, 'utf8');
console.log('ファイルの内容:', content);
} catch (error) {
if (error.code === 'ENOENT') {
console.log('エラー:ファイルが存在しません。');
} else {
console.log('予期せぬエラーが発生しました:', error.message);
}
}
}
readFileSafely('./example.txt');
この方法であれば、無駄なシステムコールを減らすことができ、かつ安全にファイルを扱うことが可能になります。
シチュエーション別の最適解まとめ
ここまで紹介した手法を、どのようなシーンで使い分けるべきかを表にまとめました。
| 手法 | 推奨されるシーン | 主なメリット |
|---|---|---|
fs.existsSync | ツールの起動時・CLIツール | コードがシンプルで直感的 |
fs.promises.access | 非同期環境での存在・権限確認 | イベントループを止めない |
fs.promises.stat | 種別(ファイル/フォルダ)の判定 | 詳細なメタデータが取得可能 |
直接 readFile 等を試行 | 直後にファイル操作を行う場合 | 最も安全でパフォーマンスが良い |
迷った場合は、「その後にファイルを操作するかどうか」で判断してください。
操作する予定があるなら直接実行し、単にUIの表示切り替えなどのために存在を知りたいだけならaccessを使用するのが最適です。
まとめ
Node.jsでのファイル存在確認は、単純に見えて奥が深いテーマです。
同期型のfs.existsSyncは手軽ですが、大規模なアプリケーションやAPIサーバーではパフォーマンス劣化の原因になる可能性があることを忘れてはいけません。
現代のNode.js開発では、fs.promisesを活用した非同期処理が推奨されており、これにより効率的でスケーラブルなコードを実現できます。
また、競合状態を避けるために、可能な限り「存在確認」と「ファイル操作」を分離せず、一つのtry...catchブロックにまとめる設計を意識しましょう。
適切なメソッドを選択することで、エラーに強く、メンテナンス性の高いアプリケーションを構築できるようになります。
