Node.js util.promisify:將 Callback 舊函式轉為 Promise

雖然現代 Node.js 開發全面擁抱 Async/Await,但在龐大的 npm 生態系中,仍有許多優秀的舊模組採用 Error-First Callback 的風格。為了讓程式碼風格統一,我們需要將這些舊 API 轉換為 Promise 形式。

使用內建的 util.promisify

Node.js util 模組提供了一個神奇的函數 promisify。只要目標函數符合「最後一個參數是 Callback」且「Callback 第一個參數是 Error」的慣例,它就能自動完成轉換。

範例:轉換檔案讀取工具

const fs = require('fs');
const { promisify } = require('util');

// 將舊式的 fs.readFile 轉換為 Promise 版本
const readFileAsync = promisify(fs.readFile);

async function getConfig() {
  try {
    const data = await readFileAsync('settings.json', 'utf8');
    console.log('配置內容:', data);
  } catch (err) {
    console.error('讀取失敗:', err.message);
  }
}

getConfig();

手動封裝 (Manual Promisification)

並非所有函式都那麼聽話。如果你遇到不符合標準規範(例如 Callback 不是最後一個參數,或沒有錯誤優先設計)的函式,你就需要手動用 new Promise 進行包裝:

// 假設這是一個舊套件的函式,且 Callback 設計不標準
function legacyTask(id, callback, options) {
  setTimeout(() => {
    callback(`任務 ${id} 完成`);
  }, 1000);
}

// 手動轉換為 Promise 版本
function taskWrapper(id, options) {
  return new Promise((resolve, reject) => {
    try {
      legacyTask(
        id,
        (result) => {
          resolve(result);
        },
        options
      );
    } catch (err) {
      reject(err);
    }
  });
}

現代 Node.js 的「去 Promisify」趨勢

值得注意的是,Node.js 官方意識到這層轉換的繁瑣,因此在常用的核心模組中,現在都直接封裝好了 Promise 版本的入口,通常位於 /promises 下。

這代表你不再需要手動呼叫 promisify

// 核心模組推薦寫法
const fs = require('fs/promises'); // 推薦
const dns = require('dns/promises');
const { setTimeout } = require('timers/promises');

async function modernApp() {
  await setTimeout(1000); // 真正非阻塞的延遲
  const data = await fs.readFile('info.txt', 'utf8');
}
反向操作:如果你極有必要將 Promise 轉回 Callback 風格,可以使用 util.callbackify,這在某些需要與舊框架橋接的場景中會用到。

總結

  • util.promisify 是銜接新舊時代的橋樑。
  • 它只對符合 Error-First 慣例的函數有效。
  • 優先尋找內建的 /promises 模組,以保持代碼簡潔。