What is promisification?

👨‍💻 Frontend Developer 🟡 Often Asked 🎚️ Medium
#JavaScript #Asynchronicity #JS Basics

Brief Answer

Promisification is the process of converting functions that use callbacks into functions that return promises. This allows using modern syntax like async/await and promise methods (.then, .catch) instead of traditional callback functions. Promisification helps avoid “Callback Hell” and makes code more readable and maintainable.

Key benefits:

  • Improved readability — linear code structure instead of nested callbacks
  • Simplified error handling — centralized handling through .catch
  • Compatibility — ability to use modern approaches with legacy code

Full Answer

Promisification is a technique in JavaScript that allows converting callback-based functions into promise-returning functions. This is especially useful when working with legacy code or libraries that don’t yet use promises.

What is Promisification

Promisification solves the “Callback Hell” problem by converting traditional callback functions into promises:

// Traditional callback
function readFileCallback(filename, callback) {
  // ... implementation
  if (error) {
    callback(error, null);
  } else {
    callback(null, data);
  }
}
 
// Promisified version
function readFilePromise(filename) {
  return new Promise((resolve, reject) => {
    readFileCallback(filename, (error, data) => {
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  });
}

Main Use Cases

1. Converting Node.js APIs

Many Node.js APIs use the callback approach:

const fs = require('fs');
 
// Callback version
fs.readFile('file.txt', 'utf8', (error, data) => {
  if (error) {
    console.error('Error:', error);
  } else {
    console.log('Data:', data);
  }
});
 
// Promisified version
function readFile(filename, encoding) {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, encoding, (error, data) => {
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  });
}
 
// Usage
readFile('file.txt', 'utf8')
  .then(data => console.log('Data:', data))
  .catch(error => console.error('Error:', error));

2. Promisifying Functions with Multiple Parameters

Functions with multiple parameters can also be promisified:

// Function with callback
function multiplyWithCallback(a, b, callback) {
  setTimeout(() => {
    if (typeof a !== 'number' || typeof b !== 'number') {
      callback(new Error('Arguments must be numbers'), null);
    } else {
      callback(null, a * b);
    }
  }, 1000);
}
 
// Promisified version
function multiply(a, b) {
  return new Promise((resolve, reject) => {
    multiplyWithCallback(a, b, (error, result) => {
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    });
  });
}
 
// Usage
multiply(5, 3)
  .then(result => console.log('Result:', result)) // 15
  .catch(error => console.error('Error:', error.message));

3. Using util.promisify

Node.js provides a built-in way to promisify:

const { promisify } = require('util');
const fs = require('fs');
 
// Promisification with util.promisify
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
 
// Usage
async function processFile() {
  try {
    const data = await readFile('input.txt', 'utf8');
    const processedData = data.toUpperCase();
    await writeFile('output.txt', processedData, 'utf8');
    console.log('File processed successfully');
  } catch (error) {
    console.error('Error processing file:', error);
  }
}

Practical Examples

Promisifying XMLHttpRequest

// Callback version
function xhrRequest(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  
  xhr.onload = function() {
    if (xhr.status === 200) {
      callback(null, xhr.responseText);
    } else {
      callback(new Error(`HTTP error: ${xhr.status}`));
    }
  };
  
  xhr.onerror = function() {
    callback(new Error('Network error'));
  };
  
  xhr.send();
}
 
// Promisified version
function fetchUrl(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    
    xhr.onload = function() {
      if (xhr.status === 200) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(`HTTP error: ${xhr.status}`));
      }
    };
    
    xhr.onerror = function() {
      reject(new Error('Network error'));
    };
    
    xhr.send();
  });
}
 
// Usage
fetchUrl('/api/data')
  .then(data => console.log('Data received:', data))
  .catch(error => console.error('Error:', error.message));

Promisifying setTimeout

// Callback version
function delayCallback(ms, callback) {
  setTimeout(() => {
    callback(null, `Passed ${ms} milliseconds`);
  }, ms);
}
 
// Promisified version
function delay(ms) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`Passed ${ms} milliseconds`);
    }, ms);
  });
}
 
// Usage with async/await
async function example() {
  console.log('Start');
  const message = await delay(2000);
  console.log(message);
  console.log('End');
}

Promisifying Functions with Multiple Results

// Callback version with multiple results
function getUserWithCallback(id, callback) {
  // Simulating async operation
  setTimeout(() => {
    if (id <= 0) {
      callback(new Error('Invalid user ID'), null);
    } else {
      // Callback receives multiple parameters
      callback(null, { id, name: `User ${id}` }, 'extra data');
    }
  }, 1000);
}
 
// Promisified version
function getUser(id) {
  return new Promise((resolve, reject) => {
    getUserWithCallback(id, (error, user, extraData) => {
      if (error) {
        reject(error);
      } else {
        // Return object with all data
        resolve({ user, extraData });
      }
    });
  });
}
 
// Usage
getUser(123)
  .then(result => {
    console.log('User:', result.user);
    console.log('Extra data:', result.extraData);
  })
  .catch(error => console.error('Error:', error.message));

Common Mistakes and Solutions

1. Improper Error Handling

// ❌ Improper error handling
function badPromisify(fn) {
  return function(...args) {
    return new Promise(resolve => {
      fn(...args, (error, result) => {
        if (error) {
          // Forgot reject
          console.error(error);
        } else {
          resolve(result);
        }
      });
    });
  };
}
 
// ✅ Proper error handling
function goodPromisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      });
    });
  };
}

2. Missing return

// ❌ Missing return
function badPromisify(fn) {
  function(...args) {  // Missing return
    return new Promise((resolve, reject) => {
      fn(...args, (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      });
    });
  }
}
 
// ✅ Proper return
function goodPromisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      });
    });
  };
}

Best Practices

  1. Always handle errors — use reject for callback errors
  2. Preserve function signature — pass all arguments to the original function
  3. Return objects for multiple results — if callback receives multiple parameters
  4. Use util.promisify — for standard Node.js functions
  5. Test promisified functions — ensure they properly handle both success and errors

Key Promisification Benefits

  1. Readability — linear structure instead of nested callbacks
  2. Error handling — centralized handling through .catch
  3. Call chains — ability to use .then for sequential operations
  4. Compatibility — integration of legacy code with modern approaches
  5. Async/await — ability to use modern syntax

Promisification is an important technique that allows modernizing legacy code using the callback approach and integrating it with modern asynchronous patterns. Understanding promisification helps write cleaner, more readable, and more maintainable code.


Knowledge Check Task

Task

Promisify the following function and explain what will be output to the console:

// Original function with callback
function calculateWithCallback(a, b, operation, callback) {
  setTimeout(() => {
    switch (operation) {
      case 'add':
        callback(null, a + b);
        break;
      case 'subtract':
        callback(null, a - b);
        break;
      case 'multiply':
        callback(null, a * b);
        break;
      default:
        callback(new Error('Unknown operation'), null);
    }
  }, 1000);
}
 
// Your task: create a&nbsp;promisified version
// and execute the following code:
 
// calculate(10, 5, 'add')
//   .then(result => {
//     console.log('Addition:', result);
//     return calculate(result, 3, 'multiply');
//   })
//   .then(result => {
//     console.log('Multiplication:', result);
//     return calculate(result, 7, 'subtract');
//   })
//   .then(result => {
//     console.log('Subtraction:', result);
//   })
//   .catch(error => {
//     console.error('Error:', error.message);
//   });
View solution

Solution:

// Promisified version
function calculate(a, b, operation) {
  return new Promise((resolve, reject) => {
    calculateWithCallback(a, b, operation, (error, result) => {
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    });
  });
}
 
// Code execution
calculate(10, 5, 'add')
  .then(result => {
    console.log('Addition:', result); // Addition: 15
    return calculate(result, 3, 'multiply');
  })
  .then(result => {
    console.log('Multiplication:', result); // Multiplication: 45
    return calculate(result, 7, 'subtract');
  })
  .then(result => {
    console.log('Subtraction:', result); // Subtraction: 38
  })
  .catch(error => {
    console.error('Error:', error.message);
  });

Explanation:

  1. Create a promisified function calculate that returns a Promise
  2. Inside the Promise, call the original function with callback
  3. In the callback, check for errors and call reject or resolve accordingly
  4. When using the .then chain, results are sequentially passed from one then to another
  5. If an error occurs anywhere, it will be caught in the .catch block

Benefits of this approach:

  • Linear code structure instead of nested callbacks
  • Centralized error handling
  • Ability to use operation chains
  • Compatibility with async/await

Want more articles to prepare for interviews? Subscribe to EasyAdvice, bookmark the site and improve yourself every day 💪