util.callbackify()

Converts an async function into one that takes an error-first callback as its last argument.

Signature
util.callbackify(original)
  • Added in Node.js v8.2.0

Parameters

ParameterTypeDescription
$originalFunctionAn `async` function, or any function that returns a Promise.

Return value

FunctionA callback-style function taking an (err, value) => ... callback as its last argument.

Errors

Throws ERR_INVALID_ARG_TYPE if `original` is not a function; the returned function throws synchronously if its last argument is not a function.

util.callbackify() is the inverse of util.promisify(). You write the implementation as an async function and hand callers a function that ends in (err, value) => ....

When to reach for it

Almost always at a boundary you do not control. The two common cases are shipping a library whose published API is callback-based while its internals have moved to async/await, and plugging an async function into a framework that only accepts node-style callbacks — older Mocha hooks, Express-era middleware chains, Gulp tasks, some AWS Lambda runtimes. If you own both sides, return the promise directly instead.

What trips people up

  • Falsy rejections get wrapped. Rejecting with null, undefined, 0 or '' would make if (err) lie, so Node substitutes an Error reading Promise was rejected with falsy value and stores the original on err.reason.
  • The callback is required. Calling the wrapper without a function as its final argument throws synchronously rather than returning a promise.
  • Callback errors are not caught. A throw inside your callback propagates as an uncaught exception, because the callback runs on process.nextTick() outside the promise chain.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Keep a published callback-based API working after rewriting the internals with async/await.
  • Hand an async function to a framework hook that only accepts node-style callbacks.
  • Adapt an async worker to an older job queue that signals completion through done(err).

Frequently asked questions

What does "Promise was rejected with falsy value" mean?

It means your async function rejected with `null`, `undefined`, `0`, `false` or `''`, and `util.callbackify()` replaced it with a real `Error` so the callback's `if (err)` check still works. The original value is available on `err.reason`. Reject with an `Error` instance to avoid the substitution.

How do I let callback users call my async function?

Wrap it with `util.callbackify()` and export the wrapper. Callers pass an `(err, value) => ...` function as the final argument; the wrapper invokes it with `null` and the resolved value, or with the rejection reason and no value.

Can the function returned by util.callbackify also return a promise?

No. The returned function always requires a callback as its last argument and returns `undefined`; omitting the callback throws synchronously. To support both conventions, export a small dispatcher that calls the original async function when no callback is supplied.

More examples

Falsy rejection reasons are wrapped in an Error

If Node passed `null` straight through, the callback's `if (err)` guard would treat a failure as a success. Instead it synthesises an `Error` and hangs the original rejection value off `err.reason`, so the error is detectable and the reason is still recoverable. Non-falsy rejection reasons are passed through untouched.

Example · javascript
const util = require('node:util');

const rejectsWithNull = util.callbackify(async () => {
  throw null; // eslint-disable-line no-throw-literal
});

rejectsWithNull((err) => {
  console.log(err instanceof Error); // true
  console.log(err.message);          // Promise was rejected with falsy value
  console.log(err.reason);           // null
});

Publishing a dual promise/callback API

The `async` implementation stays the single source of truth and `callbackify()` supplies the legacy surface. The dispatch on `typeof callback` is the standard shape for a library that must not break callback users in a minor release; both consumers here print the same user object.

Example · javascript
const util = require('node:util');

async function fetchUser(id) {
  if (!Number.isInteger(id)) throw new TypeError('id must be an integer');
  return { id, name: 'ada' };
}

const fetchUserCb = util.callbackify(fetchUser);

// One exported function that serves both calling conventions.
function getUser(id, callback) {
  if (typeof callback === 'function') {
    fetchUserCb(id, callback);
    return undefined;
  }
  return fetchUser(id);
}

module.exports = { getUser };

// Callback consumer:
getUser(1, (err, user) => console.log('cb:', err, user));

// Promise consumer:
getUser(1).then((user) => console.log('promise:', user));

Errors thrown inside the callback are not swallowed

The callback is invoked via `process.nextTick()`, deliberately outside the promise chain, so a throw inside it becomes an uncaught exception rather than a silently rejected promise. That is the correct callback-convention behaviour, but it means `callbackify()` gives you no error isolation at the boundary — wrap the callback body in try/catch if the caller cannot be trusted.

Example · javascript
const util = require('node:util');

const run = util.callbackify(async () => 'ok');

process.on('uncaughtException', (err) => {
  console.log('uncaught:', err.message);
});

run((err, value) => {
  console.log('value:', value);
  throw new Error('boom from the callback');
});

// Prints:
// value: ok
// uncaught: boom from the callback

Discussion

  • Be the first to comment on this lesson.