Permalink
Browse files

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 <benjamingr@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Myles Borins <myles.borins@gmail.com>
Reviewed-By: Evan Lucas <evanlucas@me.com>
Reviewed-By: William Kapke <william.kapke@gmail.com>
Reviewed-By: Timothy Gu <timothygu99@gmail.com>
Reviewed-By: Teddy Katz <teddy.katz@gmail.com>
  • Loading branch information...
addaleax committed Apr 14, 2017
1 parent 059f296 commit 99da8e8e02a874a0a044889f863c45700509d02c
Showing with 222 additions and 0 deletions.
  1. +82 −0 doc/api/util.md
  2. +61 −0 lib/internal/util.js
  3. +2 −0 lib/util.js
  4. +1 −0 src/node_util.cc
  5. +76 −0 test/parallel/test-util-promisify.js
View
@@ -399,6 +399,86 @@ util.inspect.defaultOptions.maxArrayLength = null;
console.log(arr); // logs the full array
```
## util.promisify(original)
<!-- YAML
added: REPLACEME
-->
* `original` {Function}
Takes a function following the common Node.js callback style, i.e. taking a
`(err, value) => ...` callback as the last argument, and returns a version
that returns promises.
For example:
```js
const util = require('util');
const fs = require('fs');
const stat = util.promisify(fs.stat);
stat('.').then((stats) => {
// Do something with `stats`
}).catch((error) => {
// Handle the error.
});
```
Or, equivalently using `async function`s:
```js
const util = require('util');
const fs = require('fs');
const stat = util.promisify(fs.stat);
async function callStat() {
const stats = await stat('.');
console.log(`This directory is owned by ${stats.uid}`);
}
```
If there is an `original[util.promisify.custom]` property present, `promisify`
will return its value, see [Custom promisified functions][].
`promisify()` assumes that `original` is a function taking a callback as its
final argument in all cases, and the returned function will result in undefined
behaviour if it does not.
### Custom promisified functions
Using the `util.promisify.custom` symbol one can override the return value of
[`util.promisify()`][]:
```js
const util = require('util');
function doSomething(foo, callback) {
// ...
}
doSomething[util.promisify.custom] = function(foo) {
return getPromiseSomehow();
};
const promisified = util.promisify(doSomething);
console.log(promisified === doSomething[util.promisify.custom]);
// prints 'true'
```
This can be useful for cases where the original function does not follow the
standard format of taking an error-first callback as the last argument.
### util.promisify.custom
<!-- YAML
added: REPLACEME
-->
* {symbol}
A Symbol that can be used to declare custom promisified variants of functions,
see [Custom promisified functions][].
## Deprecated APIs
The following APIs have been deprecated and should no longer be used. Existing
@@ -878,7 +958,9 @@ Deprecated predecessor of `console.log`.
[`console.error()`]: console.html#console_console_error_data_args
[`console.log()`]: console.html#console_console_log_data_args
[`util.inspect()`]: #util_util_inspect_object_options
[`util.promisify()`]: #util_util_promisify_original
[Custom inspection functions on Objects]: #util_custom_inspection_functions_on_objects
[Customizing `util.inspect` colors]: #util_customizing_util_inspect_colors
[Custom promisified functions]: #util_custom_promisified_functions
[constructor]: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/constructor
[semantically incompatible]: https://github.com/nodejs/node/issues/4179
View
@@ -4,6 +4,8 @@ const errors = require('internal/errors');
const binding = process.binding('util');
const signals = process.binding('constants').os.signals;
const { createPromise, promiseResolve, promiseReject } = binding;
const kArrowMessagePrivateSymbolIndex = binding['arrow_message_private_symbol'];
const kDecoratedPrivateSymbolIndex = binding['decorated_private_symbol'];
const noCrypto = !process.versions.openssl;
@@ -217,3 +219,62 @@ module.exports = exports = {
// default isEncoding implementation, just in case userland overrides it.
kIsEncodingSymbol: Symbol('node.isEncoding')
};
const kCustomPromisifiedSymbol = Symbol('util.promisify.custom');
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');
function promisify(orig) {
if (typeof orig !== 'function') {
const errors = require('internal/errors');
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'original', 'function');
}
if (orig[kCustomPromisifiedSymbol]) {
const fn = orig[kCustomPromisifiedSymbol];
if (typeof fn !== 'function') {
throw new TypeError('The [util.promisify.custom] property must be ' +
'a function');
}
Object.defineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
return fn;
}
// Names to create an object from in case the callback receives multiple
// arguments, e.g. ['stdout', 'stderr'] for child_process.exec.
const argumentNames = orig[kCustomPromisifyArgsSymbol];
function fn(...args) {
const promise = createPromise();
try {
orig.call(this, ...args, (err, ...values) => {
if (err) {
promiseReject(promise, err);
} else if (argumentNames !== undefined && values.length > 1) {
const obj = {};
for (var i = 0; i < argumentNames.length; i++)
obj[argumentNames[i]] = values[i];
promiseResolve(promise, obj);
} else {
promiseResolve(promise, values[0]);
}
});
} catch (err) {
promiseReject(promise, err);
}
return promise;
}
Object.setPrototypeOf(fn, Object.getPrototypeOf(orig));
Object.defineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
return Object.defineProperties(fn, Object.getOwnPropertyDescriptors(orig));
}
promisify.custom = kCustomPromisifiedSymbol;
exports.promisify = promisify;
exports.customPromisifyArgs = kCustomPromisifyArgsSymbol;
View
@@ -1057,3 +1057,5 @@ exports._exceptionWithHostPort = function(err,
// process.versions needs a custom function as some values are lazy-evaluated.
process.versions[exports.inspect.custom] =
(depth) => exports.format(JSON.parse(JSON.stringify(process.versions)));
exports.promisify = internalUtil.promisify;
View
@@ -21,6 +21,7 @@ using v8::Value;
#define VALUE_METHOD_MAP(V) \
V(isAsyncFunction, IsAsyncFunction) \
V(isDataView, IsDataView) \
V(isDate, IsDate) \
V(isExternal, IsExternal) \
@@ -0,0 +1,76 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const vm = require('vm');
const { promisify } = require('util');
common.crashOnUnhandledRejection();
const stat = promisify(fs.stat);
{
const promise = stat(__filename);
assert(promise instanceof Promise);
promise.then(common.mustCall((value) => {
assert.deepStrictEqual(value, fs.statSync(__filename));
}));
}
{
const promise = stat('/dontexist');
promise.catch(common.mustCall((error) => {
assert(error.message.includes('ENOENT: no such file or directory, stat'));
}));
}
{
function fn() {}
function promisifedFn() {}
fn[promisify.custom] = promisifedFn;
assert.strictEqual(promisify(fn), promisifedFn);
assert.strictEqual(promisify(promisify(fn)), promisifedFn);
}
{
function fn() {}
fn[promisify.custom] = 42;
assert.throws(
() => promisify(fn),
(err) => err instanceof TypeError &&
err.message === 'The [util.promisify.custom] property must ' +
'be a function');
}
{
const fn = vm.runInNewContext('(function() {})');
assert.notStrictEqual(Object.getPrototypeOf(promisify(fn)),
Function.prototype);
}
{
function fn(callback) {
callback(null, 'foo', 'bar');
}
promisify(fn)().then(common.mustCall((value) => {
assert.deepStrictEqual(value, 'foo');
}));
}
{
function fn(callback) {
callback(null);
}
promisify(fn)().then(common.mustCall((value) => {
assert.strictEqual(value, undefined);
}));
}
{
function fn(callback) {
callback();
}
promisify(fn)().then(common.mustCall((value) => {
assert.strictEqual(value, undefined);
}));
}

6 comments on commit 99da8e8

@aendrew

This comment has been minimized.

Show comment
Hide comment
@aendrew

aendrew May 12, 2017

🙌 * 9001 — so excited for this.

Will it be in 8.x, then?

aendrew replied May 12, 2017

🙌 * 9001 — so excited for this.

Will it be in 8.x, then?

@addaleax

This comment has been minimized.

Show comment
Hide comment
@addaleax

addaleax May 12, 2017

Member

@aendrew Yes! :) Have an RC: https://nodejs.org/download/rc/v8.0.0-rc.0/ The 8.0.0 release itself is planned for/by May 30th. :)

Member

addaleax replied May 12, 2017

@aendrew Yes! :) Have an RC: https://nodejs.org/download/rc/v8.0.0-rc.0/ The 8.0.0 release itself is planned for/by May 30th. :)

@styfle

This comment has been minimized.

Show comment
Hide comment
@styfle

styfle May 12, 2017

Contributor

@aendrew Yes it will, very exciting! I wrote some of my first thoughts on Medium.

Contributor

styfle replied May 12, 2017

@aendrew Yes it will, very exciting! I wrote some of my first thoughts on Medium.

@markstos

This comment has been minimized.

Show comment
Hide comment
@markstos

markstos May 12, 2017

Contributor

Why is this in the util. name space and not in the Promise namespace, which seems far more natural?

Contributor

markstos replied May 12, 2017

Why is this in the util. name space and not in the Promise namespace, which seems far more natural?

@addaleax

This comment has been minimized.

Show comment
Hide comment
@addaleax

addaleax May 12, 2017

Member

Why is this in the util. name space

Because it’s an utility function. If you have a better place in Node core where it should be (Promise doesn’t count for that, see below), we can revisit that. I haven’t seen any, though. :)

and not in the Promise namespace, which seems far more natural?

Because Promise is a language built-in, not something that Node offers. We try to avoid messing with built-in object as much as possible, so that users know they may rely on those having the same features available in other environments as Node.

Member

addaleax replied May 12, 2017

Why is this in the util. name space

Because it’s an utility function. If you have a better place in Node core where it should be (Promise doesn’t count for that, see below), we can revisit that. I haven’t seen any, though. :)

and not in the Promise namespace, which seems far more natural?

Because Promise is a language built-in, not something that Node offers. We try to avoid messing with built-in object as much as possible, so that users know they may rely on those having the same features available in other environments as Node.

@markstos

This comment has been minimized.

Show comment
Hide comment
@markstos

markstos May 12, 2017

Contributor

@addaleax Thanks for the prompt and clear response.

I appreciate the attempt to be compatible with other JavaScript environments. I'm part of the group of largely backend-only developers, so I don't notice the compatibility differences with JavaScript engines in browsers, but I do use a mix of native and bluebird promises on the backend. When it comes to comparing to bluebird and other promise libraries, the choice of the util namespace is confusingly different when there's already a precedent for Promise.promisify existing and working exactly the same as util.promisify().

What would be more useful to be would be:

    // All the methods from the native `Promise` namespace, 
    // plus `promisify()` in one convenient location
    var Promise = require('node.Promise')

This solution be non-conflicting-- an understandable goal-- but still clear and convenient. Loading most Promise methods from Promise but then loading just one from util. would be both confusing and inconsistent.

In my example, I use the new node. namespace for a sub-namespace that might conflict with a native namespace, but that has been modified or extended for Node.js.

Contributor

markstos replied May 12, 2017

@addaleax Thanks for the prompt and clear response.

I appreciate the attempt to be compatible with other JavaScript environments. I'm part of the group of largely backend-only developers, so I don't notice the compatibility differences with JavaScript engines in browsers, but I do use a mix of native and bluebird promises on the backend. When it comes to comparing to bluebird and other promise libraries, the choice of the util namespace is confusingly different when there's already a precedent for Promise.promisify existing and working exactly the same as util.promisify().

What would be more useful to be would be:

    // All the methods from the native `Promise` namespace, 
    // plus `promisify()` in one convenient location
    var Promise = require('node.Promise')

This solution be non-conflicting-- an understandable goal-- but still clear and convenient. Loading most Promise methods from Promise but then loading just one from util. would be both confusing and inconsistent.

In my example, I use the new node. namespace for a sub-namespace that might conflict with a native namespace, but that has been modified or extended for Node.js.

Please sign in to comment.