Browse Source

util: add callbackify

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

PR-URL: https://github.com/nodejs/node/pull/12712
Fixes: https://github.com/nodejs/CTC/issues/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>
v6
Refael Ackermann 8 years ago
parent
commit
af3aa682ac
  1. 59
      doc/api/util.md
  2. 1
      lib/internal/errors.js
  3. 50
      lib/util.js
  4. 15
      test/fixtures/uncaught-exceptions/callbackify1.js
  5. 22
      test/fixtures/uncaught-exceptions/callbackify2.js
  6. 227
      test/parallel/test-util-callbackify.js

59
doc/api/util.md

@ -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

1
lib/internal/errors.js

@ -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) {

50
lib/util.js

@ -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;

15
test/fixtures/uncaught-exceptions/callbackify1.js

@ -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
test/fixtures/uncaught-exceptions/callbackify2.js

@ -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));
}

227
test/parallel/test-util-callbackify.js

@ -0,0 +1,227 @@
'use strict';
const common = require('../common');
// This test checks that the semantics of `util.callbackify` are as described in
// the API docs
const assert = require('assert');
const { callbackify } = require('util');
const { join } = require('path');
const { execFile } = require('child_process');
const fixtureDir = join(common.fixturesDir, 'uncaught-exceptions');
const values = [
'hello world',
null,
undefined,
false,
0,
{},
{ key: 'value' },
Symbol('I am a symbol'),
function ok() {},
['array', 'with', 4, 'values'],
new Error('boo')
];
{
// Test that the resolution value is passed as second argument to callback
for (const value of values) {
// Test and `async function`
async function asyncFn() {
return await Promise.resolve(value);
}
const cbAsyncFn = callbackify(asyncFn);
cbAsyncFn(common.mustCall((err, ret) => {
assert.ifError(err);
assert.strictEqual(ret, value);
}));
// Test Promise factory
function promiseFn() {
return Promise.resolve(value);
}
const cbPromiseFn = callbackify(promiseFn);
cbPromiseFn(common.mustCall((err, ret) => {
assert.ifError(err);
assert.strictEqual(ret, value);
}));
// Test Thenable
function thenableFn() {
return {
then(onRes, onRej) {
onRes(value);
}
};
}
const cbThenableFn = callbackify(thenableFn);
cbThenableFn(common.mustCall((err, ret) => {
assert.ifError(err);
assert.strictEqual(ret, value);
}));
}
}
{
// Test that rejection reason is passed as first argument to callback
for (const value of values) {
// Test an `async function`
async function asyncFn() {
return await Promise.reject(value);
}
const cbAsyncFn = callbackify(asyncFn);
cbAsyncFn(common.mustCall((err, ret) => {
assert.strictEqual(ret, undefined);
if (err instanceof Error) {
if ('reason' in err) {
assert(!value);
assert.strictEqual(err.code, 'FALSY_VALUE_REJECTION');
assert.strictEqual(err.reason, value);
} else {
assert.strictEqual(String(value).endsWith(err.message), true);
}
} else {
assert.strictEqual(err, value);
}
}));
// test a Promise factory
function promiseFn() {
return Promise.reject(value);
}
const cbPromiseFn = callbackify(promiseFn);
cbPromiseFn(common.mustCall((err, ret) => {
assert.strictEqual(ret, undefined);
if (err instanceof Error) {
if ('reason' in err) {
assert(!value);
assert.strictEqual(err.code, 'FALSY_VALUE_REJECTION');
assert.strictEqual(err.reason, value);
} else {
assert.strictEqual(String(value).endsWith(err.message), true);
}
} else {
assert.strictEqual(err, value);
}
}));
// Test Thenable
function thenableFn() {
return {
then(onRes, onRej) {
onRej(value);
}
};
}
const cbThenableFn = callbackify(thenableFn);
cbThenableFn(common.mustCall((err, ret) => {
assert.strictEqual(ret, undefined);
if (err instanceof Error) {
if ('reason' in err) {
assert(!value);
assert.strictEqual(err.code, 'FALSY_VALUE_REJECTION');
assert.strictEqual(err.reason, value);
} else {
assert.strictEqual(String(value).endsWith(err.message), true);
}
} else {
assert.strictEqual(err, value);
}
}));
}
}
{
// Test that arguments passed to callbackified function are passed to original
for (const value of values) {
async function asyncFn(arg) {
assert.strictEqual(arg, value);
return await Promise.resolve(arg);
}
const cbAsyncFn = callbackify(asyncFn);
cbAsyncFn(value, common.mustCall((err, ret) => {
assert.ifError(err);
assert.strictEqual(ret, value);
}));
function promiseFn(arg) {
assert.strictEqual(arg, value);
return Promise.resolve(arg);
}
const cbPromiseFn = callbackify(promiseFn);
cbPromiseFn(value, common.mustCall((err, ret) => {
assert.ifError(err);
assert.strictEqual(ret, value);
}));
}
}
{
// Test that `this` binding is the same for callbackified and original
for (const value of values) {
const iAmThis = {
fn(arg) {
assert.strictEqual(this, iAmThis);
return Promise.resolve(arg);
},
};
iAmThis.cbFn = callbackify(iAmThis.fn);
iAmThis.cbFn(value, common.mustCall(function(err, ret) {
assert.ifError(err);
assert.strictEqual(ret, value);
assert.strictEqual(this, iAmThis);
}));
const iAmThat = {
async fn(arg) {
assert.strictEqual(this, iAmThat);
return await Promise.resolve(arg);
},
};
iAmThat.cbFn = callbackify(iAmThat.fn);
iAmThat.cbFn(value, common.mustCall(function(err, ret) {
assert.ifError(err);
assert.strictEqual(ret, value);
assert.strictEqual(this, iAmThat);
}));
}
}
{
// Test that callback that throws emits an `uncaughtException` event
const fixture = join(fixtureDir, 'callbackify1.js');
execFile(
process.execPath,
[fixture],
common.mustCall((err, stdout, stderr) => {
assert.strictEqual(err.code, 1);
assert.strictEqual(Object.getPrototypeOf(err).name, 'Error');
assert.strictEqual(stdout, '');
const errLines = stderr.trim().split(/[\r\n]+/g);
const errLine = errLines.find((l) => /^Error/.exec(l));
assert.strictEqual(errLine, `Error: ${fixture}`);
})
);
}
{
// Test that handled `uncaughtException` works and passes rejection reason
const fixture = join(fixtureDir, 'callbackify2.js');
execFile(
process.execPath,
[fixture],
common.mustCall((err, stdout, stderr) => {
assert.ifError(err);
assert.strictEqual(stdout.trim(), fixture);
assert.strictEqual(stderr, '');
})
);
}
Loading…
Cancel
Save