Permalink
Browse files

util: add callbackify

Add `util.callbackify(function)` for creating callback style functions
from functions returning a `Thenable`

PR-URL: #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 Apr 28, 2017
1 parent 780acc2 commit af3aa682ac534bb55765f5fef2755a88e5ff2580
View
@@ -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
@@ -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
View
@@ -164,6 +164,7 @@ E('ERR_SOCKET_BAD_PORT', 'Port should be > 0 and < 65536');
E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running');
E('ERR_V8BREAKITERATOR', 'full ICU data not installed. ' +
'See https://github.com/nodejs/node/wiki/Intl');
E('FALSY_VALUE_REJECTION', 'Promise was rejected with falsy value');
// Add new errors from here...
function invalidArgType(name, expected, actual) {
View
@@ -1047,3 +1047,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;
@@ -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);
});
}
@@ -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));
}
Oops, something went wrong.

0 comments on commit af3aa68

Please sign in to comment.