Implement promisify() — Convert Callbacks to Promises

promisify wraps a Node.js-style error-first callback function and returns a Promise-based version. Build the base version, then extend it to let the original function override the resolved value.

10 min read
JavaScript
Interview
Implementation
Promises

TABLE OF CONTENTS

promisify(fn) converts a Node.js-style callback function (arg, cb) into a Promise-returning function (arg) => Promise. The callback convention is (err, result) — error-first. Build the base version, then extend it so the original function can override the resolved value.


What is promisify()?

promisify(fn) converts a callback-based function into a Promise-based function. Specifically, it targets Node.js-style error-first callbacks where the function signature is fn(arg, (err, result) => {}). The returned function takes the same arguments (minus the callback) and returns a Promise that resolves with result or rejects with err.

This is the callback-to-Promise adapter pattern. Before Promises and async/await became standard, the entire Node.js ecosystem used error-first callbacks — fs.readFile(path, (err, data) => {}), db.query(sql, (err, rows) => {}), etc. promisify bridges that legacy world to modern async code.

Node.js ships with util.promisify() for exactly this purpose, but interviewers want to see you build it from scratch because it tests:

  1. Understanding the error-first callback convention(err, result) where the first argument signals failure
  2. Dynamic function wrapping — returning a function with ...args that internally calls the original with an added callback
  3. Promise construction — creating a Promise that resolves/rejects based on the callback's parameters

Real-world use cases:

  • Wrapping legacy Node.js APIs: const readFile = promisify(fs.readFile); await readFile("data.json")
  • Adapting third-party callback-based SDKs to modern async patterns
  • Building async middleware that bridges old and new code styles during migration

The interview often extends to version II: the original function can return a value that overrides the callback result (a pattern seen in some libraries). This tests whether you can handle the edge case where both the return value and the callback compete to settle the Promise.


The Problem

"Implement promisify(fn) that takes a function following the Node.js error-first callback convention fn(arg, (err, result) => {}) and returns a function that returns a Promise."

"Version II: Support a special return value from the original function that can override the callback result."


Thought Process

The pattern is:

  1. Return a new function that returns a Promise
  2. Inside, call the original function with the provided arguments + a custom callback
  3. The callback resolves or rejects the Promise based on err

The tricky part: the original function might pass extra arguments after err and result. Handle them by slicing arguments.


Step 1 — Base Implementation

Loading editor...


Step 2 — Preserving this Context

The interviewer says: "What if the original function uses this?"

Loading editor...

The fix: If promisify is used as a method decorator, the caller needs to .bind() the original function. This is the same behavior as the native util.promisify.


Step 3 — Multiple Success Values

Node-style callbacks sometimes pass more than two arguments: callback(null, arg1, arg2). Handle this by collecting all success arguments:

Loading editor...


Step 4 — Version II: Custom Resolve Override

Some libraries (like Node's fs.exists) don't follow the error-first convention. The original function can return true to override the callback's result:

Loading editor...


Step 5 — Edge Cases

Synchronous throw: If fn throws synchronously (not via the callback), the Promise constructor catches it — no special handling needed.

No callback called: If fn never calls the callback, the Promise hangs — same as native util.promisify. This is by design.

Extra arguments after err: callback(null, ...results) collects them all. Single result → resolve as value; multiple → resolve as array.


Full Solution

Loading editor...


What Interviewers Are Testing

  • Error-first callback convention — understanding the (err, result) pattern
  • Promise construction — wrapping callback-based APIs in Promises
  • this forwarding — preserving context with .call(this, ...)
  • Rest parameters — using ...args for flexible argument passing
  • Multiple success values — handling callback(null, a, b, c)

Complexity

TimeSpace
promisifyO(1) — just creates a wrapperO(1)

Interview Tips

  • Nail the callback signature — "The callback is error-first: (err, result). If err is truthy, reject; otherwise resolve."
  • Use rest params...args captures all arguments the wrapper receives and forwards them to the original function.
  • Handle multiple success values — "I'll use ...results to capture all non-error arguments. If there's only one, resolve with it directly; otherwise resolve with the array."
  • Mention util.promisify — Node.js has this built-in. The interviewer wants to know you understand why you'd build it vs use the native one (e.g., custom behavior).

Related Questions


Let's Connect

© 2026 Naveen Karthik // Built with React & MUI