Node.jsでのアプリケーション開発において、非同期処理をいかに効率的かつ読みやすく記述するかは、プロジェクトの保守性を高める上で極めて重要なテーマです。

かつてのNode.jsではコールバック関数を利用した非同期処理が主流でしたが、現在はPromiseやasync/awaitを用いた直感的な記述が標準となっています。

しかし、古いライブラリや一部の標準モジュールには、依然としてコールバック形式のインターフェースが残っている場合があります。

このようなコールバック形式の関数を、モダンなPromise形式に変換するために用意されている便利なツールがutil.promisifyです。

本記事では、util.promisifyの基本的な使い方から、実務で役立つ応用的なテクニックまでを詳しく解説します。

util.promisifyとは何か

util.promisifyは、Node.jsの標準モジュールであるutilに含まれているユーティリティ関数です。

この関数の主な役割は、「エラー優先コールバック」という形式の関数を、Promiseを返す関数にラップして変換することにあります。

Node.jsにおけるエラー優先コールバックとは、関数の最後の引数にコールバック関数を受け取り、その第一引数にエラーオブジェクト、第二引数以降に結果を渡すという設計パターンのことです。

util.promisifyを使用することで、古いスタイルのコードを書き換えることなく、最新のasync/await構文の中でシームレスに利用できるようになります。

これにより、いわゆる「コールバック地獄」と呼ばれるネストの深いコードを解消し、上から下へ流れるような読みやすいプログラムを記述することが可能になります。

基本的な使い方と構文

util.promisifyを利用するためには、まずutilモジュールをインポートする必要があります。

ここでは、代表的な例として、ファイルシステムを扱うfsモジュールの関数をPromise化する手順を見ていきましょう。

以下のコードは、コールバック形式のfs.readFileをPromise形式に変換し、ファイルの内容を読み込む例です。

JavaScript
const util = require('util');
const fs = require('fs');

// fs.readFileをPromiseを返す関数に変換します
const readFilePromisified = util.promisify(fs.readFile);

async function readMyFile() {
    try {
        // Promise化された関数をawaitで呼び出します
        const data = await readFilePromisified('./example.txt', 'utf8');
        console.log('ファイルの内容:', data);
    } catch (err) {
        // エラーが発生した場合はcatchブロックで捕捉できます
        console.error('読み込み失敗:', err);
    }
}

readMyFile();
実行結果
ファイルの内容: (ファイルの中身が表示されます)

このように、従来のfs.readFile(path, encoding, (err, data) => { ... })という形式を記述する必要がなくなります。

エラーハンドリングがtry...catchで統一できる点も、大きなメリットの一つと言えるでしょう。

なぜutil.promisifyが必要なのか

現代のNode.js開発においては、多くの標準APIにPromise版(例:fs.promises)が用意されています。

しかし、Node.jsの全機能がPromise化されているわけではなく、一部のレガシーな関数やサードパーティ製のライブラリでは、依然としてコールバック方式が採用されています。

また、自作した古い関数群を一度に書き直すコストをかけられない場合にも、util.promisifyは非常に有効な手段となります。

一貫性のないコードベースにおいて、非同期処理のインターフェースをPromiseに統一することは、開発チーム全体の生産性向上に直結します。

さらに、Promiseベースにすることで、Promise.allPromise.raceといった強力な制御フロー関数を組み合わせて利用できるようになります。

エラー優先コールバックのルール

util.promisifyが正しく動作するためには、変換対象の関数が特定のルールに従っている必要があります。

以下の条件を満たさない関数に対してutil.promisifyを使用すると、期待通りの動作をしない可能性があるため注意が必要です。

項目条件の詳細
引数の構成関数の最後の引数がコールバック関数であること。
コールバックの第一引数エラーが発生した際にエラーオブジェクトを受け取ること。
成功時の処理エラーがない場合、第一引数はnullまたはundefinedであること。

このパターンは「Node.js形式のコールバック」や「エラー優先コールバック」と呼ばれ、Node.jsの設計哲学の根幹をなしています。

もし、独自のルールでコールバックを実装している関数をPromise化したい場合は、後述するカスタムPromise化の手法を検討してください。

実践:外部コマンドの実行をPromise化する

別の実用的な例として、child_processモジュールのexec関数をPromise化するケースを考えてみましょう。

execはシェルコマンドを実行するために頻繁に使われますが、標準ではコールバック形式です。

これをPromise化することで、スクリプトの実行順序をより明確に制御できるようになります。

JavaScript
const { exec } = require('child_process');
const util = require('util');

// exec関数をPromise化します
const execPromise = util.promisify(exec);

async function checkNodeVersion() {
    try {
        // コマンドを実行し、標準出力を受け取ります
        const { stdout, stderr } = await execPromise('node -v');
        
        if (stderr) {
            console.error('エラー出力:', stderr);
            return;
        }
        
        console.log('Node.jsバージョン:', stdout.trim());
    } catch (error) {
        console.error('実行エラー:', error);
    }
}

checkNodeVersion();
実行結果
Node.jsバージョン: v20.x.x (実行環境のバージョンが表示されます)

execのように、コールバックに複数の引数(この場合はstdoutstderr)を渡す関数の場合、util.promisify複数の値をプロパティとして持つオブジェクトを返します。

分割代入を利用することで、必要な値だけをスマートに抽出できるため、非常に相性が良いと言えます。

util.promisify.customによるカスタマイズ

時には、標準的なエラー優先コールバックの形式に従っていない関数をPromise化したい場合があります。

そのようなケースでは、関数オブジェクトに特殊なシンボルutil.promisify.customを定義することで、独自の変換ロジックを指定できます。

これにより、ライブラリの作者はユーザーに対してutil.promisify経由で最適なPromise体験を提供することが可能になります。

JavaScript
const util = require('util');

// 特殊なコールバック形式を持つ関数
function myComplexFunction(arg1, callback) {
    // 成功時に (result, error) という特殊な順番で返すと仮定
    setTimeout(() => {
        callback('処理成功', null);
    }, 100);
}

// カスタムのPromise化ロジックを手動で定義します
myComplexFunction[util.promisify.custom] = (arg1) => {
    return new Promise((resolve, reject) => {
        myComplexFunction(arg1, (result, error) => {
            if (error) {
                reject(error);
            } else {
                resolve(result);
            }
        });
    });
};

const customPromisified = util.promisify(myComplexFunction);

async function run() {
    const result = await customPromisified('test');
    console.log(result);
}

run();
実行結果
処理成功

このように、シンボルプロパティを使用することで既存の挙動をオーバーライドできるため、柔軟な対応が可能です。

注意点:コンテキスト(this)の喪失

util.promisifyを使用する際に最も注意すべき点は、メソッドが所属するオブジェクトのコンテキスト(this)が失われることです。

クラスのインスタンスメソッドなどをそのままPromise化して呼び出すと、内部でthisを参照している場合にエラーが発生します。

この問題を回避するためには、bindメソッドを使用してコンテキストを明示的に固定する必要があります。

JavaScript
const util = require('util');

const database = {
    connectionString: 'localhost:5432',
    query: function(sql, callback) {
        // this.connectionString を参照するため、コンテキストが必要
        console.log(`Connecting to ${this.connectionString}...`);
        setTimeout(() => callback(null, `Result for ${sql}`), 100);
    }
};

// そのままでは失敗するため、bind(database) でコンテキストを固定します
const querySafe = util.promisify(database.query).bind(database);

async function doQuery() {
    const res = await querySafe('SELECT * FROM users');
    console.log(res);
}

doQuery();

オブジェクトのメソッドをPromise化する際は、常に「この関数の中でthisを使っているか?」を確認する癖をつけることが重要です。

この知識があるだけで、デバッグの時間を大幅に短縮できるはずです。

現代の代替手段:fs/promisesなどの活用

ここまでutil.promisifyの活用法を解説してきましたが、最新のNode.jsではより簡単な選択肢も増えています。

例えば、fsdnsreadlineなどの主要なモジュールには、最初からPromiseを返すメソッド群がpromisesサブパスとして提供されています。

これらの公式Promise版が存在する場合は、わざわざutil.promisifyを使う必要はなく、直接そちらを利用するのがベストプラクティスです。

JavaScript
// util.promisify を使わなくても、最初からPromise版をインポートできます
const fs = require('fs').promises;

async function modernRead() {
    const content = await fs.readFile('./test.txt', 'utf-8');
    console.log(content);
}

ただし、公式にPromise版が用意されていないマイナーな関数や、サードパーティ製ライブラリを扱う際には、引き続きutil.promisifyが主役となります。

状況に応じて、これらを適切に使い分けられるようになることが、プロフェッショナルなNode.jsエンジニアへの近道です。

まとめ

util.promisifyは、Node.jsの歴史的な背景を持つコールバック形式のコードと、現代的なPromiseベースのコードを繋ぐ重要な架け橋です。

このツールを使いこなすことで、複雑な非同期処理を簡潔にまとめ、コードの可読性とメンテナンス性を劇的に向上させることができます。

最後に、本記事で紹介した重要なポイントを振り返ります。

  • util.promisifyはエラー優先コールバック形式の関数をPromise形式に変換する。
  • async/awaitと組み合わせることで、エラーハンドリングをtry...catchに集約できる。
  • child_process.execのように、複数の戻り値がある場合はオブジェクトとして返される。
  • 特殊な関数にはutil.promisify.customを使って独自の変換ロジックを実装できる。
  • クラスメソッドなどを変換する場合は、.bind()によるthisの固定が必要になる場合がある。
  • 標準モジュールにPromise版が存在する場合は、そちらを優先的に使用する。

非同期処理の扱いに長けることは、堅牢なNode.jsアプリケーションを構築するための第一歩です。

今回学んだテクニックを、ぜひ日々のコーディングやレガシーコードの改善に役立ててください。