Skip to content

Commit

Permalink
util: add util.promisify()
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
addaleax committed May 9, 2017
1 parent 059f296 commit 99da8e8
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 0 deletions.
82 changes: 82 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
61 changes: 61 additions & 0 deletions lib/internal/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
2 changes: 2 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
1 change: 1 addition & 0 deletions src/node_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ using v8::Value;


#define VALUE_METHOD_MAP(V) \
V(isAsyncFunction, IsAsyncFunction) \
V(isDataView, IsDataView) \
V(isDate, IsDate) \
V(isExternal, IsExternal) \
Expand Down
76 changes: 76 additions & 0 deletions test/parallel/test-util-promisify.js
Original file line number Diff line number Diff line change
@@ -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

@aendra-rininsland
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌 * 9001 — so excited for this.

Will it be in 8.x, then?

@addaleax
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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
Copy link
Member

@styfle styfle commented on 99da8e8 May 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@markstos
Copy link
Contributor

@markstos markstos commented on 99da8e8 May 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@addaleax
Copy link
Member Author

@addaleax addaleax commented on 99da8e8 May 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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