Navigation

Node.js

Node.js util.promisify(): Convert Callbacks to Promises (2025)

Learn how to use Node.js util.promisify() to transform callback-based functions into promises. Master async/await patterns with practical examples and best practices.

Table Of Contents

Introduction

JavaScript's evolution from callback-heavy code to promise-based patterns has revolutionized how developers handle asynchronous operations. However, many Node.js applications still rely on legacy callback-based APIs, creating a disconnect between modern async/await syntax and older codebases.

This presents a common challenge: how do you modernize callback-based functions without rewriting entire applications? The answer lies in Node.js's built-in util.promisify() method, a powerful utility that bridges the gap between callbacks and promises.

In this comprehensive guide, you'll learn how to leverage util.promisify() to transform callback-based functions into promise-returning equivalents, enabling cleaner, more maintainable code with async/await patterns. We'll explore practical examples, advanced use cases, and best practices that will elevate your Node.js development skills.

What is util.promisify()?

The util.promisify() method is a built-in Node.js utility that converts callback-based functions into promise-returning functions. Introduced in Node.js 8.0.0, this method follows a simple convention: it takes a function that expects a callback as its last argument and returns a new function that returns a promise instead.

The Problem with Callbacks

Traditional Node.js APIs use the error-first callback pattern:

const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Error reading file:', err);
    return;
  }
  console.log('File contents:', data);
});

While functional, this approach leads to several issues:

  • Callback hell: Nested callbacks become difficult to read and maintain
  • Error handling complexity: Multiple error-handling paths
  • Inconsistent patterns: Mixing callbacks with modern promise-based code

The Solution: Promisification

Using util.promisify(), you can transform the same function into a promise-based equivalent:

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

const readFileAsync = util.promisify(fs.readFile);

// Now you can use async/await
async function readFileExample() {
  try {
    const data = await readFileAsync('example.txt', 'utf8');
    console.log('File contents:', data);
  } catch (err) {
    console.error('Error reading file:', err);
  }
}

Basic Syntax and Usage

Core Syntax

The basic syntax of util.promisify() is straightforward:

const util = require('util');
const promisifiedFunction = util.promisify(callbackFunction);

Requirements for Promisification

For util.promisify() to work correctly, the original function must follow these conventions:

  1. Error-first callback: The callback should be the last parameter
  2. Standard signature: callback(error, result) where error is null on success
  3. Single result: The callback should pass only one result value (excluding the error)

Simple Example: setTimeout

Let's start with a basic example using setTimeout:

const util = require('util');

// Traditional callback approach
setTimeout(() => {
  console.log('Timer finished');
}, 1000);

// Promisified version
const delay = util.promisify(setTimeout);

async function timerExample() {
  console.log('Starting timer...');
  await delay(1000);
  console.log('Timer finished');
}

timerExample();

Real-World Examples and Use Cases

File System Operations

File system operations are prime candidates for promisification:

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

// Promisify multiple fs methods
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const mkdir = util.promisify(fs.mkdir);
const stat = util.promisify(fs.stat);

async function fileOperations() {
  try {
    // Check if directory exists
    try {
      await stat('output');
    } catch (err) {
      // Directory doesn't exist, create it
      await mkdir('output');
    }

    // Read source file
    const content = await readFile('input.txt', 'utf8');
    
    // Process content (example: convert to uppercase)
    const processedContent = content.toUpperCase();
    
    // Write to new file
    await writeFile('output/processed.txt', processedContent);
    
    console.log('File processing completed successfully');
  } catch (error) {
    console.error('File operation failed:', error);
  }
}

Database Operations

Transform database callbacks into promises for cleaner async code:

const sqlite3 = require('sqlite3').verbose();
const util = require('util');

class Database {
  constructor(path) {
    this.db = new sqlite3.Database(path);
    this.run = util.promisify(this.db.run.bind(this.db));
    this.get = util.promisify(this.db.get.bind(this.db));
    this.all = util.promisify(this.db.all.bind(this.db));
  }

  async createTable() {
    await this.run(`
      CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE NOT NULL
      )
    `);
  }

  async addUser(name, email) {
    const result = await this.run(
      'INSERT INTO users (name, email) VALUES (?, ?)',
      [name, email]
    );
    return result.lastID;
  }

  async getUser(id) {
    return await this.get('SELECT * FROM users WHERE id = ?', [id]);
  }

  async getAllUsers() {
    return await this.all('SELECT * FROM users');
  }
}

// Usage example
async function databaseExample() {
  const db = new Database('users.db');
  
  await db.createTable();
  const userId = await db.addUser('John Doe', 'john@example.com');
  const user = await db.getUser(userId);
  
  console.log('Created user:', user);
}

HTTP Requests

Promisify HTTP request libraries for better async handling:

const request = require('request');
const util = require('util');

const requestAsync = util.promisify(request);

async function fetchData(url) {
  try {
    const response = await requestAsync({
      url: url,
      json: true,
      timeout: 5000
    });

    if (response.statusCode === 200) {
      return response.body;
    } else {
      throw new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`);
    }
  } catch (error) {
    console.error('Request failed:', error.message);
    throw error;
  }
}

// Usage
async function apiExample() {
  try {
    const data = await fetchData('https://api.example.com/users');
    console.log('API Response:', data);
  } catch (error) {
    console.error('Failed to fetch data:', error);
  }
}

Advanced Techniques and Custom Implementations

Handling Non-Standard Callbacks

Some functions don't follow the standard error-first callback pattern. You can create custom promisification:

const util = require('util');

// Function with non-standard callback signature
function customCallback(value, callback) {
  setTimeout(() => {
    if (value > 0) {
      callback(value * 2); // Success, no error parameter
    } else {
      callback(null); // Failure indicated by null
    }
  }, 100);
}

// Custom promisify wrapper
function promisifyCustom(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (result) => {
        if (result === null) {
          reject(new Error('Operation failed'));
        } else {
          resolve(result);
        }
      });
    });
  };
}

const customCallbackAsync = promisifyCustom(customCallback);

async function customExample() {
  try {
    const result = await customCallbackAsync(5);
    console.log('Result:', result); // 10
  } catch (error) {
    console.error('Error:', error.message);
  }
}

Bulk Promisification

Promisify multiple methods of an object at once:

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

function promisifyObject(obj, methods) {
  const promisified = {};
  
  methods.forEach(method => {
    if (typeof obj[method] === 'function') {
      promisified[method] = util.promisify(obj[method]).bind(obj);
    }
  });
  
  return promisified;
}

// Promisify multiple fs methods
const fsAsync = promisifyObject(fs, [
  'readFile', 'writeFile', 'readdir', 'stat', 'mkdir', 'rmdir'
]);

async function bulkExample() {
  try {
    const files = await fsAsync.readdir('./');
    console.log('Directory contents:', files);
    
    for (const file of files) {
      const stats = await fsAsync.stat(file);
      console.log(`${file}: ${stats.isDirectory() ? 'Directory' : 'File'}`);
    }
  } catch (error) {
    console.error('Directory operation failed:', error);
  }
}

Using util.promisify.custom

For functions that need special promisification behavior, use the custom symbol:

const util = require('util');

function specialFunction(callback) {
  // Original implementation
  setTimeout(() => callback(null, 'original result'), 100);
}

// Add custom promisify behavior
specialFunction[util.promisify.custom] = function() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('custom promisified result'), 50);
  });
};

const promisifiedSpecial = util.promisify(specialFunction);

async function customSymbolExample() {
  const result = await promisifiedSpecial();
  console.log(result); // 'custom promisified result'
}

Performance Considerations and Best Practices

Performance Optimization Tips

  1. Reuse promisified functions: Don't recreate them on every call
// ❌ Inefficient - creates new promisified function each time
async function badExample() {
  const readFile = util.promisify(fs.readFile);
  return await readFile('file.txt', 'utf8');
}

// ✅ Efficient - reuse promisified function
const readFile = util.promisify(fs.readFile);
async function goodExample() {
  return await readFile('file.txt', 'utf8');
}
  1. Cache promisified functions: Store them for repeated use
const promisified = new Map();

function getPromisified(fn) {
  if (!promisified.has(fn)) {
    promisified.set(fn, util.promisify(fn));
  }
  return promisified.get(fn);
}
  1. Use native promise versions when available: Many Node.js modules now provide promise-based APIs
const fsPromises = require('fs').promises;

// ✅ Use native promises when available
async function useNativePromises() {
  const data = await fsPromises.readFile('file.txt', 'utf8');
  return data;
}

Error Handling Best Practices

  1. Always wrap in try-catch blocks:
async function robustErrorHandling() {
  try {
    const data = await readFileAsync('file.txt', 'utf8');
    return data;
  } catch (error) {
    // Log error for debugging
    console.error('File read failed:', error);
    
    // Provide fallback or re-throw with context
    throw new Error(`Failed to read configuration file: ${error.message}`);
  }
}
  1. Handle specific error types:
async function specificErrorHandling() {
  try {
    const data = await readFileAsync('config.json', 'utf8');
    return JSON.parse(data);
  } catch (error) {
    if (error.code === 'ENOENT') {
      console.log('Config file not found, using defaults');
      return getDefaultConfig();
    } else if (error instanceof SyntaxError) {
      throw new Error('Invalid JSON in config file');
    } else {
      throw error; // Re-throw unexpected errors
    }
  }
}

Common Pitfalls to Avoid

  1. Don't promisify already promisified functions:
// ❌ Avoid double promisification
const readFile = util.promisify(fs.readFile);
const doublePromisified = util.promisify(readFile); // Wrong!

// ✅ Promisify only the original callback function
const readFile = util.promisify(fs.readFile);
  1. Be careful with this context:
class FileManager {
  constructor() {
    this.encoding = 'utf8';
  }

  // ❌ Wrong - loses 'this' context
  read(filename, callback) {
    fs.readFile(filename, this.encoding, callback);
  }
}

// ✅ Correct - bind the context
const fileManager = new FileManager();
const readMethod = util.promisify(fileManager.read.bind(fileManager));
  1. Handle multiple callback arguments properly:
// Some functions pass multiple values to callback
function multipleValues(callback) {
  callback(null, 'value1', 'value2', 'value3');
}

// util.promisify only returns the first value
const promisified = util.promisify(multipleValues);
const result = await promisified(); // Only gets 'value1'

// Custom solution for multiple values
function promisifyMultiple(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (err, ...values) => {
        if (err) reject(err);
        else resolve(values);
      });
    });
  };
}

Frequently Asked Questions

What types of functions can be promisified with util.promisify()?

The function must follow the error-first callback convention where the callback is the last parameter and has the signature (error, result). Functions like fs.readFile, setTimeout, and most Node.js core API methods work perfectly. Functions with non-standard callback signatures require custom promisification approaches.

Can I use util.promisify() with functions that have multiple callback parameters?

No, util.promisify() only works with functions that pass a single result value to the callback (excluding the error). If a function passes multiple values like callback(null, value1, value2), you'll need to create a custom promisification wrapper that returns an array or object containing all values.

Is there a performance difference between callbacks and promisified functions?

Promisified functions have a slight overhead due to the promise wrapper, but the difference is negligible in most applications. The benefits of cleaner, more maintainable code with async/await typically outweigh the minimal performance cost. For high-performance scenarios, consider using native promise-based APIs when available.

How do I handle functions that don't follow the error-first callback pattern?

You'll need to create a custom promisification wrapper. Analyze the callback signature and create a function that translates the callback pattern into a standard promise resolution/rejection. The util.promisify.custom symbol can also be used to define custom promisification behavior for specific functions.

Can I promisify methods from third-party libraries?

Yes, as long as they follow the error-first callback convention. However, many modern libraries already provide promise-based APIs. Check the documentation first to see if native promises are available, as they're often more efficient and better integrated than promisified callback versions.

What's the difference between util.promisify() and manually creating promises?

util.promisify() provides a standardized, tested approach that handles edge cases and maintains proper error propagation. Manual promise creation gives you more control but requires careful attention to error handling, proper cleanup, and avoiding memory leaks. Use util.promisify() for standard callback functions and manual promises for complex custom logic.

Conclusion

Mastering util.promisify() is essential for modernizing Node.js applications and bridging the gap between legacy callback-based code and modern async/await patterns. Here are the key takeaways from this comprehensive guide:

Essential Benefits: util.promisify() transforms callback-heavy code into clean, readable async/await syntax while maintaining compatibility with existing APIs. This leads to better error handling, reduced callback hell, and more maintainable codebases.

Best Practices: Always reuse promisified functions for better performance, handle errors with try-catch blocks, and prefer native promise APIs when available. Remember to bind the correct context for object methods and avoid double promisification.

Advanced Techniques: For non-standard callbacks, create custom promisification wrappers or use the util.promisify.custom symbol. Consider bulk promisification for objects with multiple methods you need to convert.

Performance Considerations: While promisified functions have minimal overhead, the improved code maintainability and developer experience make them worthwhile for most applications. Cache promisified functions and use native promise APIs when possible for optimal performance.

Ready to modernize your Node.js codebase? Start by identifying callback-heavy areas in your application and gradually implement util.promisify() to transform them into promise-based patterns. Share your experience with promisification in the comments below – what challenges did you face, and how did this approach improve your code quality?

For more advanced Node.js tips and best practices, subscribe to our newsletter and stay updated with the latest developments in JavaScript and Node.js ecosystem.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Node.js