Skip to content

Commit

Permalink
util: add callbackify
Browse files Browse the repository at this point in the history
Add `util.callbackify(function)` for creating callback style functions
from functions returning a `Thenable`

PR-URL: nodejs#12712
Fixes: nodejs/CTC#109
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Teddy Katz <teddy.katz@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Timothy Gu <timothygu99@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
  • Loading branch information
refack committed Jun 17, 2017
1 parent 5238fa2 commit 13c4fd6
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 0 deletions.
59 changes: 59 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,64 @@ module developers as well. It can be accessed using:
const util = require('util');
```

## util.callbackify(original)
<!-- YAML
added: REPLACEME
-->

* `original` {Function} An `async` function
* Returns: {Function} a callback style function

Takes an `async` function (or a function that returns a Promise) and returns a
function following the Node.js error first callback style. In the callback, the
first argument will be the rejection reason (or `null` if the Promise resolved),
and the second argument will be the resolved value.

For example:

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

async function fn() {
return await Promise.resolve('hello world');
}
const callbackFunction = util.callbackify(fn);

callbackFunction((err, ret) => {
if (err) throw err;
console.log(ret);
});
```

Will print:

```txt
hello world
```

*Note*:

* The callback is executed asynchronously, and will have a limited stack trace.
If the callback throws, the process will emit an [`'uncaughtException'`][]
event, and if not handled will exit.

* Since `null` has a special meaning as the first argument to a callback, if a
wrapped function rejects a `Promise` with a falsy value as a reason, the value
is wrapped in an `Error` with the original value stored in a field named
`reason`.
```js
function fn() {
return Promise.reject(null);
}
const callbackFunction = util.callbackify(fn);

callbackFunction((err, ret) => {
// When the Promise was rejected with `null` it is wrapped with an Error and
// the original value is stored in `reason`.
err && err.hasOwnProperty('reason') && err.reason === null; // true
});
```

## util.debuglog(section)
<!-- YAML
added: v0.11.3
Expand Down Expand Up @@ -955,6 +1013,7 @@ Deprecated predecessor of `console.log`.
[`Object.assign()`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
[`console.error()`]: console.html#console_console_error_data_args
[`console.log()`]: console.html#console_console_log_data_args
[`'uncaughtException'`]: process.html#process_event_uncaughtexception
[`util.inspect()`]: #util_util_inspect_object_options
[`util.promisify()`]: #util_util_promisify_original
[Custom inspection functions on Objects]: #util_custom_inspection_functions_on_objects
Expand Down
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ E('ERR_SOCKET_BAD_TYPE',
E('ERR_SOCKET_CANNOT_SEND', 'Unable to send data');
E('ERR_SOCKET_BAD_PORT', 'Port should be > 0 and < 65536');
E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running');
E('FALSY_VALUE_REJECTION', 'Promise was rejected with falsy value');
// Add new errors from here...

function invalidArgType(name, expected, actual) {
Expand Down
50 changes: 50 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -1055,3 +1055,53 @@ process.versions[exports.inspect.custom] =
(depth) => exports.format(JSON.parse(JSON.stringify(process.versions)));

exports.promisify = internalUtil.promisify;

function callbackifyOnRejected(reason, cb) {
// `!reason` guard inspired by bluebird (Ref: https://goo.gl/t5IS6M).
// Because `null` is a special error value in callbacks which means "no error
// occurred", we error-wrap so the callback consumer can distinguish between
// "the promise rejected with null" or "the promise fulfilled with undefined".
if (!reason) {
const newReason = new errors.Error('FALSY_VALUE_REJECTION');
newReason.reason = reason;
reason = newReason;
Error.captureStackTrace(reason, callbackifyOnRejected);
}
return cb(reason);
}


function callbackify(original) {
if (typeof original !== 'function') {
throw new errors.TypeError(
'ERR_INVALID_ARG_TYPE',
'original',
'function');
}

// We DO NOT return the promise as it gives the user a false sense that
// the promise is actually somehow related to the callback's execution
// and that the callback throwing will reject the promise.
function callbackified(...args) {
const maybeCb = args.pop();
if (typeof maybeCb !== 'function') {
throw new errors.TypeError(
'ERR_INVALID_ARG_TYPE',
'last argument',
'function');
}
const cb = (...args) => { Reflect.apply(maybeCb, this, args); };
// In true node style we process the callback on `nextTick` with all the
// implications (stack, `uncaughtException`, `async_hooks`)
Reflect.apply(original, this, args)
.then((ret) => process.nextTick(cb, null, ret),
(rej) => process.nextTick(callbackifyOnRejected, rej, cb));
}

Object.setPrototypeOf(callbackified, Object.getPrototypeOf(original));
Object.defineProperties(callbackified,
Object.getOwnPropertyDescriptors(original));
return callbackified;
}

exports.callbackify = callbackify;
15 changes: 15 additions & 0 deletions test/fixtures/uncaught-exceptions/callbackify1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

// Used to test that `uncaughtException` is emitted

const { callbackify } = require('util');

{
async function fn() { }

const cbFn = callbackify(fn);

cbFn((err, ret) => {
throw new Error(__filename);
});
}
22 changes: 22 additions & 0 deletions test/fixtures/uncaught-exceptions/callbackify2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

// Used to test the `uncaughtException` err object

const assert = require('assert');
const { callbackify } = require('util');

{
const sentinel = new Error(__filename);
process.once('uncaughtException', (err) => {
assert.strictEqual(err, sentinel);
// Calling test will use `stdout` to assert value of `err.message`
console.log(err.message);
});

async function fn() {
return await Promise.reject(sentinel);
}

const cbFn = callbackify(fn);
cbFn((err, ret) => assert.ifError(err));
}
Loading

0 comments on commit 13c4fd6

Please sign in to comment.