Introduction

Node.js util.promisify() converts error-first callback functions to promise-returning functions. The convention is that the callback's first argument is the error (null on success) and the second is the result. When the original function does not follow this convention -- such as callbacks where the first argument is the result (not an error), or callbacks with multiple result arguments -- promisify either rejects incorrectly or loses data. This error commonly occurs when promisifying third-party libraries, custom callbacks, or functions that use non-standard callback signatures.

Symptoms

Promise rejects when the operation actually succeeded:

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

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

// This works fine - fs.readFile follows error-first convention readFile('/etc/hosts', 'utf8').then(data => console.log(data)); ```

But with a non-standard callback:

```javascript // Custom function where first arg is the result, not error function fetchData(callback) { // Returns (result, metadata) instead of (error, result) callback({ data: [1, 2, 3] }, { cached: true }); }

const fetchDataAsync = util.promisify(fetchData);

fetchDataAsync() .then(result => console.log(result)) // .catch(err => console.error(err)); // Outputs: { data: [1, 2, 3] } - the FIRST arg is treated as error! // The { cached: true } metadata is lost ```

Or with null first argument:

```javascript function getConfig(key, callback) { const value = configStore.get(key); callback(null, value); // null error = success }

const getConfigAsync = util.promisify(getConfig);

getConfigAsync('database.url') .then(value => console.log(value)) // Works correctly - null first arg means no error ```

Common Causes

  • Callback does not follow error-first convention: First argument is data, not error
  • Multiple result arguments: Callback receives (err, result1, result2) -- promisify only returns result1
  • Callback called with null as first result: callback(null, data) -- this IS correct for error-first
  • Function has additional parameters before callback: fn(a, b, callback) -- promisify handles this
  • Custom callback signature: Library uses (data, err) instead of (err, data)
  • Callback not called: Function returns without calling the callback, promise never resolves

Step-by-Step Fix

Step 1: Wrap non-standard callbacks

```javascript const util = require('util');

// Function with non-standard callback: (result, metadata) function fetchData(callback) { callback({ data: [1, 2, 3] }, { cached: true }); }

// Wrap to follow error-first convention function fetchDataErrorFirst(callback) { fetchData((result, metadata) => { callback(null, { result, metadata }); }); }

const fetchDataAsync = util.promisify(fetchDataErrorFirst);

fetchDataAsync().then(({ result, metadata }) => { console.log(result); // { data: [1, 2, 3] } console.log(metadata); // { cached: true } }); ```

Step 2: Handle callbacks with multiple results

```javascript // Function: (err, rows, fields) function query(sql, callback) { db.execute(sql, (err, rows, fields) => { callback(err, rows, fields); }); }

// promisify only returns the first result (rows), fields is lost const queryAsync = util.promisify(query);

// Custom wrapper to return all results function queryFull(sql) { return new Promise((resolve, reject) => { query(sql, (err, rows, fields) => { if (err) return reject(err); resolve({ rows, fields }); }); }); }

// Usage const { rows, fields } = await queryFull('SELECT * FROM users'); ```

Step 3: Use custom promisify with Symbol

```javascript const util = require('util');

function fetchData(callback) { callback({ data: [1, 2, 3] }, { cached: true }); }

// Define custom promisify logic fetchData[util.promisify.custom] = async function() { return new Promise((resolve, reject) => { fetchData((result, metadata) => { resolve({ result, metadata }); }); }); };

// Now util.promisify uses our custom logic const fetchDataAsync = util.promisify(fetchData);

const { result, metadata } = await fetchDataAsync(); ```

Prevention

  • Always verify the callback follows the (error, result) convention before promisifying
  • Wrap non-standard callbacks to match the error-first pattern
  • Use util.promisify.custom to define custom promisification logic
  • Test promisified functions with both success and error scenarios
  • Use fs.promises instead of util.promisify(fs.readFile) -- built-in promise APIs are preferred
  • Check if the library already provides a promise-based API before using promisify
  • Document callback conventions in your project's coding standards