Callback APIs hand your function to the library. You depend on it to call you once, asynchronously, with correct arguments, and errors thrown inside your callback cannot reach any outer try/catch. Several dependent steps nest into a pyramid, with an error check at every level. A promise turns that into a value you hold: it settles once, handlers always run asynchronously, and one catch covers the chain.
To convert, wrap the call at the lowest level, once, and use the promise everywhere else:
- Return
new Promise((resolve, reject) => …). - Call the original function with your arguments plus a callback.
- In the callback,
reject(error)when the first argument is truthy, otherwiseresolve(value).
That is exactly the Node convention: an error-first callback, (error, value) => …. Node ships it as util.promisify(fn). Many Node APIs already have promise versions, so check first: node:fs/promises, node:timers/promises, node:dns/promises.
Details:
- Other signatures (success and failure callbacks,
setTimeout(cb, ms)argument order,fs.exists(path, cb)) are not error-first. Wrap them by hand, or definefn[util.promisify.custom]sopromisifyreturns your promise-based version. Node does this forsetTimeoutandsetImmediate, sopromisify(setTimeout)(ms, value)works. - Several result values: a promise carries one value. Resolve with an array or object.
this: a detached method loses its receiver. Bind it or use.call.- Callbacks that fire more than once (event streams,
setInterval) cannot be promises, because a promise settles once. Use events,events.onceor async iterators. - A synchronous throw inside the original function becomes a rejection, because it happens inside the executor.
The reverse is util.callbackify.