deep-dive2025-03-28·8·135/348

Node.js util.promisify 활용법

Converting callback-based functions to Promise-based using util.promisify for cleaner async/await code in Node.js.

Node.js util.promisify 활용법

Introduction

Many Node.js core modules and third-party libraries still use the callback pattern. util.promisify converts these callback-based functions into Promise-based ones, enabling modern async/await syntax. This post covers advanced usage patterns.

Environment

node --version
# v20.11.0

Problem

Callback-based code is hard to read and error-prone:

const fs = require('fs');

// Callback hell
fs.readFile('data.json', 'utf8', (err, data) => {
  if (err) {
    console.error(err);
    return;
  }
  
  const parsed = JSON.parse(data);
  
  fs.writeFile('output.json', JSON.stringify(parsed, null, 2), (err) => {
    if (err) {
      console.error(err);
      return;
    }
    
    console.log('Done');
  });
});

Analysis

The callback pattern follows a specific convention:

// Standard Node.js callback signature
function callback(err, result) {
  // First argument is always error
  // Second argument is the result
}

// promisify expects this pattern
const fn = util.promisify(callbackBasedFunction);
const result = await fn(arg1, arg2);

The promisify function detects:

  1. The last parameter is a callback
  2. The callback follows the (err, result) pattern
  3. Custom result extraction if needed

Solution

Solution 1: Basic promisify usage

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

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

async function processData() {
  const data = await readFile('data.json', 'utf8');
  const parsed = JSON.parse(data);
  await writeFile('output.json', JSON.stringify(parsed, null, 2));
  console.log('Done');
}

processData().catch(console.error);

Solution 2: Promisify with custom result extraction

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

// Custom promisify for functions that don't follow (err, result) pattern
const pbkdf2 = promisify(crypto.pbkdf2);

async function hashPassword(password, salt) {
  const key = await pbkdf2(password, salt, 100000, 64, 'sha512');
  return key.toString('hex');
}

// Usage
hashPassword('mypassword', 'randomsalt')
  .then(console.log)
  .catch(console.error);

Solution 3: Promisify entire modules

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

// Create promisified version of fs
const fsAsync = {
  readFile: promisify(fs.readFile),
  writeFile: promisify(fs.writeFile),
  access: promisify(fs.access),
  stat: promisify(fs.stat),
  readdir: promisify(fs.readdir)
};

// Usage
async function listFiles(dir) {
  const entries = await fsAsync.readdir(dir);
  const files = [];
  
  for (const entry of entries) {
    const stat = await fsAsync.stat(`${dir}/${entry}`);
    if (stat.isFile()) {
      files.push(entry);
    }
  }
  
  return files;
}

Solution 4: Promisify with custom logic

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

// For functions with non-standard callback positions
function customPromisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      const callback = (err, ...results) => {
        if (err) {
          reject(err);
        } else {
          resolve(results.length === 1 ? results[0] : results);
        }
      };
      
      fn.call(this, ...args, callback);
    });
  };
}

// Example: A function with callback in the middle
function fetchMultiple(urls, callback) {
  // callback is not the last parameter
}

// Use custom promisify if needed

Solution 5: Complete async file operations

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

const fsAsync = {
  readFile: promisify(fs.readFile),
  writeFile: promisify(fs.writeFile),
  mkdir: promisify(fs.mkdir),
  access: promisify(fs.access),
  stat: promisify(fs.stat),
  readdir: promisify(fs.readdir),
  unlink: promisify(fs.unlink),
  rename: promisify(fs.rename)
};

class AsyncFileManager {
  constructor(basePath) {
    this.basePath = basePath;
  }
  
  async ensureDir(dirPath) {
    try {
      await fsAsync.access(dirPath);
    } catch {
      await fsAsync.mkdir(dirPath, { recursive: true });
    }
  }
  
  async readJSON(filename) {
    const filePath = path.join(this.basePath, filename);
    const data = await fsAsync.readFile(filePath, 'utf8');
    return JSON.parse(data);
  }
  
  async writeJSON(filename, data) {
    await this.ensureDir(path.dirname(path.join(this.basePath, filename)));
    const filePath = path.join(this.basePath, filename);
    await fsAsync.writeFile(filePath, JSON.stringify(data, null, 2));
  }
  
  async copyFile(src, dest) {
    const data = await fsAsync.readFile(src);
    await fsAsync.writeFile(dest, data);
  }
  
  async deleteFile(filename) {
    const filePath = path.join(this.basePath, filename);
    await fsAsync.unlink(filePath);
  }
}

// Usage
const manager = new AsyncFileManager('./data');

(async () => {
  await manager.writeJSON('config.json', { key: 'value' });
  const config = await manager.readJSON('config.json');
  console.log(config);
})();

Lessons Learned

  1. Use promisify for callback-based core modules - fs, crypto, dns, etc.
  2. Check if the library already has a Promise API - Many modern libraries do
  3. Handle errors properly - Promise rejections must be caught
  4. Consider using fs/promises - Node.js 14+ has built-in Promise-based fs
  5. Promisify is synchronous - The conversion happens at import time

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.