Skip to content

Commit 99da8e8

Browse files
committed
util: add util.promisify()
Add `util.promisify(function)` for creating promisified functions. Includes documentation and tests. Fixes: nodejs/CTC#12 PR-URL: #12442 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Myles Borins <[email protected]> Reviewed-By: Evan Lucas <[email protected]> Reviewed-By: William Kapke <[email protected]> Reviewed-By: Timothy Gu <[email protected]> Reviewed-By: Teddy Katz <[email protected]>
1 parent 059f296 commit 99da8e8

File tree

5 files changed

+222
-0
lines changed

5 files changed

+222
-0
lines changed

doc/api/util.md

+82
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,86 @@ util.inspect.defaultOptions.maxArrayLength = null;
399399
console.log(arr); // logs the full array
400400
```
401401

402+
## util.promisify(original)
403+
<!-- YAML
404+
added: REPLACEME
405+
-->
406+
407+
* `original` {Function}
408+
409+
Takes a function following the common Node.js callback style, i.e. taking a
410+
`(err, value) => ...` callback as the last argument, and returns a version
411+
that returns promises.
412+
413+
For example:
414+
415+
```js
416+
const util = require('util');
417+
const fs = require('fs');
418+
419+
const stat = util.promisify(fs.stat);
420+
stat('.').then((stats) => {
421+
// Do something with `stats`
422+
}).catch((error) => {
423+
// Handle the error.
424+
});
425+
```
426+
427+
Or, equivalently using `async function`s:
428+
429+
```js
430+
const util = require('util');
431+
const fs = require('fs');
432+
433+
const stat = util.promisify(fs.stat);
434+
435+
async function callStat() {
436+
const stats = await stat('.');
437+
console.log(`This directory is owned by ${stats.uid}`);
438+
}
439+
```
440+
441+
If there is an `original[util.promisify.custom]` property present, `promisify`
442+
will return its value, see [Custom promisified functions][].
443+
444+
`promisify()` assumes that `original` is a function taking a callback as its
445+
final argument in all cases, and the returned function will result in undefined
446+
behaviour if it does not.
447+
448+
### Custom promisified functions
449+
450+
Using the `util.promisify.custom` symbol one can override the return value of
451+
[`util.promisify()`][]:
452+
453+
```js
454+
const util = require('util');
455+
456+
function doSomething(foo, callback) {
457+
// ...
458+
}
459+
460+
doSomething[util.promisify.custom] = function(foo) {
461+
return getPromiseSomehow();
462+
};
463+
464+
const promisified = util.promisify(doSomething);
465+
console.log(promisified === doSomething[util.promisify.custom]);
466+
// prints 'true'
467+
```
468+
469+
This can be useful for cases where the original function does not follow the
470+
standard format of taking an error-first callback as the last argument.
471+
472+
### util.promisify.custom
473+
<!-- YAML
474+
added: REPLACEME
475+
-->
476+
477+
* {symbol}
478+
479+
A Symbol that can be used to declare custom promisified variants of functions,
480+
see [Custom promisified functions][].
481+
402482
## Deprecated APIs
403483

404484
The following APIs have been deprecated and should no longer be used. Existing
@@ -878,7 +958,9 @@ Deprecated predecessor of `console.log`.
878958
[`console.error()`]: console.html#console_console_error_data_args
879959
[`console.log()`]: console.html#console_console_log_data_args
880960
[`util.inspect()`]: #util_util_inspect_object_options
961+
[`util.promisify()`]: #util_util_promisify_original
881962
[Custom inspection functions on Objects]: #util_custom_inspection_functions_on_objects
882963
[Customizing `util.inspect` colors]: #util_customizing_util_inspect_colors
964+
[Custom promisified functions]: #util_custom_promisified_functions
883965
[constructor]: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/constructor
884966
[semantically incompatible]: https://github.com/nodejs/node/issues/4179

lib/internal/util.js

+61
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const errors = require('internal/errors');
44
const binding = process.binding('util');
55
const signals = process.binding('constants').os.signals;
66

7+
const { createPromise, promiseResolve, promiseReject } = binding;
8+
79
const kArrowMessagePrivateSymbolIndex = binding['arrow_message_private_symbol'];
810
const kDecoratedPrivateSymbolIndex = binding['decorated_private_symbol'];
911
const noCrypto = !process.versions.openssl;
@@ -217,3 +219,62 @@ module.exports = exports = {
217219
// default isEncoding implementation, just in case userland overrides it.
218220
kIsEncodingSymbol: Symbol('node.isEncoding')
219221
};
222+
223+
const kCustomPromisifiedSymbol = Symbol('util.promisify.custom');
224+
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');
225+
226+
function promisify(orig) {
227+
if (typeof orig !== 'function') {
228+
const errors = require('internal/errors');
229+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'original', 'function');
230+
}
231+
232+
if (orig[kCustomPromisifiedSymbol]) {
233+
const fn = orig[kCustomPromisifiedSymbol];
234+
if (typeof fn !== 'function') {
235+
throw new TypeError('The [util.promisify.custom] property must be ' +
236+
'a function');
237+
}
238+
Object.defineProperty(fn, kCustomPromisifiedSymbol, {
239+
value: fn, enumerable: false, writable: false, configurable: true
240+
});
241+
return fn;
242+
}
243+
244+
// Names to create an object from in case the callback receives multiple
245+
// arguments, e.g. ['stdout', 'stderr'] for child_process.exec.
246+
const argumentNames = orig[kCustomPromisifyArgsSymbol];
247+
248+
function fn(...args) {
249+
const promise = createPromise();
250+
try {
251+
orig.call(this, ...args, (err, ...values) => {
252+
if (err) {
253+
promiseReject(promise, err);
254+
} else if (argumentNames !== undefined && values.length > 1) {
255+
const obj = {};
256+
for (var i = 0; i < argumentNames.length; i++)
257+
obj[argumentNames[i]] = values[i];
258+
promiseResolve(promise, obj);
259+
} else {
260+
promiseResolve(promise, values[0]);
261+
}
262+
});
263+
} catch (err) {
264+
promiseReject(promise, err);
265+
}
266+
return promise;
267+
}
268+
269+
Object.setPrototypeOf(fn, Object.getPrototypeOf(orig));
270+
271+
Object.defineProperty(fn, kCustomPromisifiedSymbol, {
272+
value: fn, enumerable: false, writable: false, configurable: true
273+
});
274+
return Object.defineProperties(fn, Object.getOwnPropertyDescriptors(orig));
275+
}
276+
277+
promisify.custom = kCustomPromisifiedSymbol;
278+
279+
exports.promisify = promisify;
280+
exports.customPromisifyArgs = kCustomPromisifyArgsSymbol;

lib/util.js

+2
Original file line numberDiff line numberDiff line change
@@ -1057,3 +1057,5 @@ exports._exceptionWithHostPort = function(err,
10571057
// process.versions needs a custom function as some values are lazy-evaluated.
10581058
process.versions[exports.inspect.custom] =
10591059
(depth) => exports.format(JSON.parse(JSON.stringify(process.versions)));
1060+
1061+
exports.promisify = internalUtil.promisify;

src/node_util.cc

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ using v8::Value;
2121

2222

2323
#define VALUE_METHOD_MAP(V) \
24+
V(isAsyncFunction, IsAsyncFunction) \
2425
V(isDataView, IsDataView) \
2526
V(isDate, IsDate) \
2627
V(isExternal, IsExternal) \

test/parallel/test-util-promisify.js

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const fs = require('fs');
5+
const vm = require('vm');
6+
const { promisify } = require('util');
7+
8+
common.crashOnUnhandledRejection();
9+
10+
const stat = promisify(fs.stat);
11+
12+
{
13+
const promise = stat(__filename);
14+
assert(promise instanceof Promise);
15+
promise.then(common.mustCall((value) => {
16+
assert.deepStrictEqual(value, fs.statSync(__filename));
17+
}));
18+
}
19+
20+
{
21+
const promise = stat('/dontexist');
22+
promise.catch(common.mustCall((error) => {
23+
assert(error.message.includes('ENOENT: no such file or directory, stat'));
24+
}));
25+
}
26+
27+
{
28+
function fn() {}
29+
function promisifedFn() {}
30+
fn[promisify.custom] = promisifedFn;
31+
assert.strictEqual(promisify(fn), promisifedFn);
32+
assert.strictEqual(promisify(promisify(fn)), promisifedFn);
33+
}
34+
35+
{
36+
function fn() {}
37+
fn[promisify.custom] = 42;
38+
assert.throws(
39+
() => promisify(fn),
40+
(err) => err instanceof TypeError &&
41+
err.message === 'The [util.promisify.custom] property must ' +
42+
'be a function');
43+
}
44+
45+
{
46+
const fn = vm.runInNewContext('(function() {})');
47+
assert.notStrictEqual(Object.getPrototypeOf(promisify(fn)),
48+
Function.prototype);
49+
}
50+
51+
{
52+
function fn(callback) {
53+
callback(null, 'foo', 'bar');
54+
}
55+
promisify(fn)().then(common.mustCall((value) => {
56+
assert.deepStrictEqual(value, 'foo');
57+
}));
58+
}
59+
60+
{
61+
function fn(callback) {
62+
callback(null);
63+
}
64+
promisify(fn)().then(common.mustCall((value) => {
65+
assert.strictEqual(value, undefined);
66+
}));
67+
}
68+
69+
{
70+
function fn(callback) {
71+
callback();
72+
}
73+
promisify(fn)().then(common.mustCall((value) => {
74+
assert.strictEqual(value, undefined);
75+
}));
76+
}

0 commit comments

Comments
 (0)