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.0Problem
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:
- The last parameter is a callback
- The callback follows the
(err, result)pattern - 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 neededSolution 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
- Use promisify for callback-based core modules - fs, crypto, dns, etc.
- Check if the library already has a Promise API - Many modern libraries do
- Handle errors properly - Promise rejections must be caught
- Consider using
fs/promises- Node.js 14+ has built-in Promise-based fs - Promisify is synchronous - The conversion happens at import time
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.