Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: add util.promisify() #12442

Merged
merged 8 commits into from
May 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions doc/api/child_process.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,23 @@ child runs longer than `timeout` milliseconds.
*Note: Unlike the exec(3) POSIX system call, `child_process.exec()` does not
replace the existing process and uses a shell to execute the command.*

If this method is invoked as its [`util.promisify()`][]ed version, it returns
a Promise for an object with `stdout` and `stderr` properties.

Copy link
Member

Choose a reason for hiding this comment

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

I understand that it is quite tedious to do so, but examples would be good for these

Copy link
Member

Choose a reason for hiding this comment

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

On it.

Copy link
Member

Choose a reason for hiding this comment

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

For example:

```js
const util = require('util');
const exec = util.promisify(require('child_process').exec);

async function lsExample() {
const {stdout, stderr} = await exec('ls');
console.log('stdout:', stdout);
console.log('stderr:', stderr);
}
lsExample();
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I might be pedantic here, but we are ignoring the returned promise.

Copy link
Member Author

Choose a reason for hiding this comment

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

@thefourtheye Yes … got a better suggestion? Just leave this line out? ;)

Copy link
Contributor

Choose a reason for hiding this comment

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

Mmmm, to think of it, that sounds better to me.

```

### child_process.execFile(file[, args][, options][, callback])
<!-- YAML
added: v0.1.91
Expand Down Expand Up @@ -263,6 +280,19 @@ can be used to specify the character encoding used to decode the stdout and
stderr output. If `encoding` is `'buffer'`, or an unrecognized character
encoding, `Buffer` objects will be passed to the callback instead.

If this method is invoked as its [`util.promisify()`][]ed version, it returns
a Promise for an object with `stdout` and `stderr` properties.

```js
const util = require('util');
const execFile = util.promisify(require('child_process').execFile);
async function getVersion() {
const {stdout} = await execFile('node', ['--version']);
console.log(stdout);
}
getVersion();
```

### child_process.fork(modulePath[, args][, options])
<!-- YAML
added: v0.5.0
Expand Down Expand Up @@ -1269,4 +1299,5 @@ to `stdout` although there are only 4 characters.
[`process.on('message')`]: process.html#process_event_message
[`process.send()`]: process.html#process_process_send_message_sendhandle_options_callback
[`stdio`]: #child_process_options_stdio
[`util.promisify()`]: util.html#util_util_promisify_original
[synchronous counterparts]: #child_process_synchronous_process_creation
8 changes: 8 additions & 0 deletions doc/api/dns.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ dns.lookup('example.com', options, (err, addresses) =>
// addresses: [{"address":"2606:2800:220:1:248:1893:25c8:1946","family":6}]
```

If this method is invoked as its [`util.promisify()`][]ed version, and `all`
is not set to `true`, it returns a Promise for an object with `address` and
`family` properties.

### Supported getaddrinfo flags

The following flags can be passed as hints to [`dns.lookup()`][].
Expand Down Expand Up @@ -163,6 +167,9 @@ dns.lookupService('127.0.0.1', 22, (err, hostname, service) => {
});
```

If this method is invoked as its [`util.promisify()`][]ed version, it returns a
Promise for an object with `hostname` and `service` properties.

## dns.resolve(hostname[, rrtype], callback)
<!-- YAML
added: v0.1.27
Expand Down Expand Up @@ -528,3 +535,4 @@ uses. For instance, _they do not use the configuration from `/etc/hosts`_.
[Implementation considerations section]: #dns_implementation_considerations
[supported `getaddrinfo` flags]: #dns_supported_getaddrinfo_flags
[the official libuv documentation]: https://docs.libuv.org/en/latest/threadpool.html
[`util.promisify()`]: util.html#util_util_promisify_original
11 changes: 9 additions & 2 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,9 @@ If `position` is `null`, data will be read from the current file position.

The callback is given the three arguments, `(err, bytesRead, buffer)`.

If this method is invoked as its [`util.promisify()`][]ed version, it returns
a Promise for an object with `bytesRead` and `buffer` properties.

## fs.readdir(path[, options], callback)
<!-- YAML
added: v0.1.8
Expand Down Expand Up @@ -2393,8 +2396,11 @@ an integer specifying the number of bytes to write.
should be written. If `typeof position !== 'number'`, the data will be written
at the current position. See pwrite(2).

The callback will be given three arguments `(err, written, buffer)` where
`written` specifies how many _bytes_ were written from `buffer`.
The callback will be given three arguments `(err, bytesWritten, buffer)` where
`bytesWritten` specifies how many _bytes_ were written from `buffer`.

If this method is invoked as its [`util.promisify()`][]ed version, it returns
Copy link
Member

Choose a reason for hiding this comment

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

Going to add an example here too.

Copy link
Member

Choose a reason for hiding this comment

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

Did addaleax@07acf08 for timers, but not sure I can add an example here as there is no non-promise version here.

a Promise for an object with `bytesWritten` and `buffer` properties.

Note that it is unsafe to use `fs.write` multiple times on the same file
without waiting for the callback. For this scenario,
Expand Down Expand Up @@ -2810,6 +2816,7 @@ The following constants are meant for use with the [`fs.Stats`][] object's
[`net.Socket`]: net.html#net_class_net_socket
[`stat()`]: fs.html#fs_fs_stat_path_callback
[`util.inspect(stats)`]: util.html#util_util_inspect_object_options
[`util.promisify()`]: util.html#util_util_promisify_original
[Caveats]: #fs_caveats
[Common System Errors]: errors.html#errors_common_system_errors
[FS Constants]: #fs_fs_constants_1
Expand Down
38 changes: 38 additions & 0 deletions doc/api/timers.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,27 @@ next event loop iteration.

If `callback` is not a function, a [`TypeError`][] will be thrown.

*Note*: This method has a custom variant for promises that is available using
[`util.promisify()`][]:

```js
const util = require('util');
const setImmediatePromise = util.promisify(setImmediate);

setImmediatePromise('foobar').then((value) => {
// value === 'foobar' (passing values is optional)
// This is executed after all I/O callbacks.
});
Copy link
Member

Choose a reason for hiding this comment

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

An async function example would be cool as well.


// or with async function
async function timerExample() {
console.log('Before I/O callbacks');
await setImmediatePromise();
console.log('After I/O callbacks');
}
timerExample();
```

### setInterval(callback, delay[, ...args])
<!-- YAML
added: v0.0.1
Expand Down Expand Up @@ -126,12 +147,28 @@ will be set to `1`.

If `callback` is not a function, a [`TypeError`][] will be thrown.

*Note*: This method has a custom variant for promises that is available using
[`util.promisify()`][]:

```js
const util = require('util');
const setTimeoutPromise = util.promisify(setTimeout);

setTimeoutPromise(40, 'foobar').then((value) => {
// value === 'foobar' (passing values is optional)
// This is executed after about 40 milliseconds.
});
```

## Cancelling Timers

Copy link
Member

Choose a reason for hiding this comment

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

Perhaps add a note here that it is not yet possible to cancel a timer promise given that there is no standard way of canceling a promise.

Copy link
Member Author

Choose a reason for hiding this comment

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

done, but I didn’t mention the reason for why that doesn’t exist – I don’t think we do that in our docs

The [`setImmediate()`][], [`setInterval()`][], and [`setTimeout()`][] methods
each return objects that represent the scheduled timers. These can be used to
cancel the timer and prevent it from triggering.

It is not possible to cancel timers that were created using the promisified
variants of [`setImmediate()`][], [`setTimeout()`][].

### clearImmediate(immediate)
<!-- YAML
added: v0.9.1
Expand Down Expand Up @@ -168,4 +205,5 @@ Cancels a `Timeout` object created by [`setTimeout()`][].
[`setImmediate()`]: timers.html#timers_setimmediate_callback_args
[`setInterval()`]: timers.html#timers_setinterval_callback_delay_args
[`setTimeout()`]: timers.html#timers_settimeout_callback_delay_args
[`util.promisify()`]: util.html#util_util_promisify_original
[the Node.js Event Loop]: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick
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.
Copy link
Member

Choose a reason for hiding this comment

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

What happens if a Sync function is passed to this? Is the behavior undefined? Is there any way that we could define any reasonable behavior? (I doubt it... I'm +1 on simply saying that the behavior is undefined)

Copy link

Choose a reason for hiding this comment

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

Do you mean Sync as in calls the callback right away or as in never calls the callback? In the latter case, my expectation would be that the promise is neither resolved nor rejected - that seems well-defined.

Copy link
Member

Choose a reason for hiding this comment

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

sync functions that throw reject the promise (new Promise catches it) and sync functions that return a value resolve it with that.

In other words, it works just fine.


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}`);
}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Everywhere else the function invocation is also shown.

Copy link
Member Author

Choose a reason for hiding this comment

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

@thefourtheye I’m confused now, this seems to be the opposite of #12442 (comment) … would you prefer an extra callStat(); line or not?

Copy link
Contributor

@thefourtheye thefourtheye May 9, 2017

Choose a reason for hiding this comment

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

Sorry. We can get rid of the function calls.


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()`][]:
Copy link
Member

Choose a reason for hiding this comment

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

This should likely go into more detail on why someone would want to do this

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a sentence on what I assume is the main use case (alternative callback formats)


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

Copy link
Member

Choose a reason for hiding this comment

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

* {symbol}

* {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
10 changes: 9 additions & 1 deletion lib/child_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
'use strict';

const util = require('util');
const { deprecate, convertToValidSignal } = require('internal/util');
const {
deprecate, convertToValidSignal, customPromisifyArgs
} = require('internal/util');
const debug = util.debuglog('child_process');

const uv = process.binding('uv');
Expand Down Expand Up @@ -138,6 +140,9 @@ exports.exec = function(command /*, options, callback*/) {
opts.callback);
};

Object.defineProperty(exports.exec, customPromisifyArgs,
{ value: ['stdout', 'stderr'], enumerable: false });


exports.execFile = function(file /*, args, options, callback*/) {
var args = [];
Expand Down Expand Up @@ -333,6 +338,9 @@ exports.execFile = function(file /*, args, options, callback*/) {
return child;
};

Object.defineProperty(exports.execFile, customPromisifyArgs,
{ value: ['stdout', 'stderr'], enumerable: false });

const _deprecatedCustomFds = deprecate(
function deprecateCustomFds(options) {
options.stdio = options.customFds.map(function mapCustomFds(fd) {
Expand Down
7 changes: 7 additions & 0 deletions lib/dns.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const util = require('util');
const cares = process.binding('cares_wrap');
const uv = process.binding('uv');
const internalNet = require('internal/net');
const { customPromisifyArgs } = require('internal/util');

const GetAddrInfoReqWrap = cares.GetAddrInfoReqWrap;
const GetNameInfoReqWrap = cares.GetNameInfoReqWrap;
Expand Down Expand Up @@ -189,6 +190,9 @@ function lookup(hostname, options, callback) {
return req;
}

Object.defineProperty(lookup, customPromisifyArgs,
{ value: ['address', 'family'], enumerable: false });


function onlookupservice(err, host, service) {
if (err)
Expand Down Expand Up @@ -228,6 +232,9 @@ function lookupService(host, port, callback) {
return req;
}

Object.defineProperty(lookupService, customPromisifyArgs,
{ value: ['hostname', 'service'], enumerable: false });


function onresolve(err, result, ttls) {
if (ttls && this.ttl)
Expand Down
6 changes: 6 additions & 0 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,9 @@ fs.read = function(fd, buffer, offset, length, position, callback) {
binding.read(fd, buffer, offset, length, position, req);
};

Object.defineProperty(fs.read, internalUtil.customPromisifyArgs,
{ value: ['bytesRead', 'buffer'], enumerable: false });

fs.readSync = function(fd, buffer, offset, length, position) {
if (length === 0) {
return 0;
Expand Down Expand Up @@ -706,6 +709,9 @@ fs.write = function(fd, buffer, offset, length, position, callback) {
return binding.writeString(fd, buffer, offset, length, req);
};

Object.defineProperty(fs.write, internalUtil.customPromisifyArgs,
{ value: ['bytesWritten', 'buffer'], enumerable: false });

// usage:
// fs.writeSync(fd, buffer[, offset[, length[, position]]]);
// OR
Expand Down
Loading