Skip to content

Commit 8cba959

Browse files
refackaddaleax
authored andcommitted
util: add callbackify
Add `util.callbackify(function)` for creating callback style functions from functions returning a `Thenable` Original-PR-URL: #12712 Fixes: nodejs/CTC#109 Original-Reviewed-By: Benjamin Gruenbaum <[email protected]> Original-Reviewed-By: Teddy Katz <[email protected]> Original-Reviewed-By: Matteo Collina <[email protected]> Original-Reviewed-By: Colin Ihrig <[email protected]> Original-Reviewed-By: Timothy Gu <[email protected]> Original-Reviewed-By: Anna Henningsen <[email protected]> PR-URL: #13750 Reviewed-By: Anna Henningsen <[email protected]>
1 parent 7794030 commit 8cba959

File tree

6 files changed

+375
-0
lines changed

6 files changed

+375
-0
lines changed

doc/api/util.md

+59
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,64 @@ module developers as well. It can be accessed using:
1010
const util = require('util');
1111
```
1212

13+
## util.callbackify(original)
14+
<!-- YAML
15+
added: REPLACEME
16+
-->
17+
18+
* `original` {Function} An `async` function
19+
* Returns: {Function} a callback style function
20+
21+
Takes an `async` function (or a function that returns a Promise) and returns a
22+
function following the Node.js error first callback style. In the callback, the
23+
first argument will be the rejection reason (or `null` if the Promise resolved),
24+
and the second argument will be the resolved value.
25+
26+
For example:
27+
28+
```js
29+
const util = require('util');
30+
31+
async function fn() {
32+
return await Promise.resolve('hello world');
33+
}
34+
const callbackFunction = util.callbackify(fn);
35+
36+
callbackFunction((err, ret) => {
37+
if (err) throw err;
38+
console.log(ret);
39+
});
40+
```
41+
42+
Will print:
43+
44+
```txt
45+
hello world
46+
```
47+
48+
*Note*:
49+
50+
* The callback is executed asynchronously, and will have a limited stack trace.
51+
If the callback throws, the process will emit an [`'uncaughtException'`][]
52+
event, and if not handled will exit.
53+
54+
* Since `null` has a special meaning as the first argument to a callback, if a
55+
wrapped function rejects a `Promise` with a falsy value as a reason, the value
56+
is wrapped in an `Error` with the original value stored in a field named
57+
`reason`.
58+
```js
59+
function fn() {
60+
return Promise.reject(null);
61+
}
62+
const callbackFunction = util.callbackify(fn);
63+
64+
callbackFunction((err, ret) => {
65+
// When the Promise was rejected with `null` it is wrapped with an Error and
66+
// the original value is stored in `reason`.
67+
err && err.hasOwnProperty('reason') && err.reason === null; // true
68+
});
69+
```
70+
1371
## util.debuglog(section)
1472
<!-- YAML
1573
added: v0.11.3
@@ -955,6 +1013,7 @@ Deprecated predecessor of `console.log`.
9551013
[`Object.assign()`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
9561014
[`console.error()`]: console.html#console_console_error_data_args
9571015
[`console.log()`]: console.html#console_console_log_data_args
1016+
[`'uncaughtException'`]: process.html#process_event_uncaughtexception
9581017
[`util.inspect()`]: #util_util_inspect_object_options
9591018
[`util.promisify()`]: #util_util_promisify_original
9601019
[Custom inspection functions on Objects]: #util_custom_inspection_functions_on_objects

lib/internal/errors.js

+1
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ E('ERR_SOCKET_BAD_TYPE',
149149
E('ERR_SOCKET_CANNOT_SEND', 'Unable to send data');
150150
E('ERR_SOCKET_BAD_PORT', 'Port should be > 0 and < 65536');
151151
E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running');
152+
E('FALSY_VALUE_REJECTION', 'Promise was rejected with falsy value');
152153
// Add new errors from here...
153154

154155
function invalidArgType(name, expected, actual) {

lib/util.js

+51
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const uv = process.binding('uv');
2525
const Buffer = require('buffer').Buffer;
2626
const internalUtil = require('internal/util');
2727
const binding = process.binding('util');
28+
const errors = require('internal/errors');
2829

2930
const isError = internalUtil.isError;
3031

@@ -1055,3 +1056,53 @@ process.versions[exports.inspect.custom] =
10551056
(depth) => exports.format(JSON.parse(JSON.stringify(process.versions)));
10561057

10571058
exports.promisify = internalUtil.promisify;
1059+
1060+
function callbackifyOnRejected(reason, cb) {
1061+
// `!reason` guard inspired by bluebird (Ref: https://goo.gl/t5IS6M).
1062+
// Because `null` is a special error value in callbacks which means "no error
1063+
// occurred", we error-wrap so the callback consumer can distinguish between
1064+
// "the promise rejected with null" or "the promise fulfilled with undefined".
1065+
if (!reason) {
1066+
const newReason = new errors.Error('FALSY_VALUE_REJECTION');
1067+
newReason.reason = reason;
1068+
reason = newReason;
1069+
Error.captureStackTrace(reason, callbackifyOnRejected);
1070+
}
1071+
return cb(reason);
1072+
}
1073+
1074+
1075+
function callbackify(original) {
1076+
if (typeof original !== 'function') {
1077+
throw new errors.TypeError(
1078+
'ERR_INVALID_ARG_TYPE',
1079+
'original',
1080+
'function');
1081+
}
1082+
1083+
// We DO NOT return the promise as it gives the user a false sense that
1084+
// the promise is actually somehow related to the callback's execution
1085+
// and that the callback throwing will reject the promise.
1086+
function callbackified(...args) {
1087+
const maybeCb = args.pop();
1088+
if (typeof maybeCb !== 'function') {
1089+
throw new errors.TypeError(
1090+
'ERR_INVALID_ARG_TYPE',
1091+
'last argument',
1092+
'function');
1093+
}
1094+
const cb = (...args) => { Reflect.apply(maybeCb, this, args); };
1095+
// In true node style we process the callback on `nextTick` with all the
1096+
// implications (stack, `uncaughtException`, `async_hooks`)
1097+
Reflect.apply(original, this, args)
1098+
.then((ret) => process.nextTick(cb, null, ret),
1099+
(rej) => process.nextTick(callbackifyOnRejected, rej, cb));
1100+
}
1101+
1102+
Object.setPrototypeOf(callbackified, Object.getPrototypeOf(original));
1103+
Object.defineProperties(callbackified,
1104+
Object.getOwnPropertyDescriptors(original));
1105+
return callbackified;
1106+
}
1107+
1108+
exports.callbackify = callbackify;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
// Used to test that `uncaughtException` is emitted
4+
5+
const { callbackify } = require('util');
6+
7+
{
8+
async function fn() { }
9+
10+
const cbFn = callbackify(fn);
11+
12+
cbFn((err, ret) => {
13+
throw new Error(__filename);
14+
});
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
3+
// Used to test the `uncaughtException` err object
4+
5+
const assert = require('assert');
6+
const { callbackify } = require('util');
7+
8+
{
9+
const sentinel = new Error(__filename);
10+
process.once('uncaughtException', (err) => {
11+
assert.strictEqual(err, sentinel);
12+
// Calling test will use `stdout` to assert value of `err.message`
13+
console.log(err.message);
14+
});
15+
16+
async function fn() {
17+
return await Promise.reject(sentinel);
18+
}
19+
20+
const cbFn = callbackify(fn);
21+
cbFn((err, ret) => assert.ifError(err));
22+
}

0 commit comments

Comments
 (0)