Table of contents:
Introduction
This article discusses several methods for creating a wrapper for an non-blocking asynchronous function that produces a synchronous blocking function.
What we'll discuss:
-
How to use
node:util.promisify
to wrap a non-blocking asynchronous function to return a promise and then useawait
to block and get the full-filled value to that promise. -
How to write a wrapper by creating a promise with
new Promise(...)
that waits on an asynchronous function and then wait on and get the value of that. -
How to write a function that generates the wrapper.
Note that this is a follow-up and extension to my earlier post "Concurrency - Notes for Node.js and JavaScript".
A few references:
Promisify
For information, see util.promisify(original).
As you notice, if you read at the above link, util.promisify
is
only directly usable on asynchronous functions that follow the
standard interface. So, if the function does not take as its
argument a callback function whose arguments are (error, data)
.
So that raises the question, what if you want to wrap a function that does not follow that standard interface. An obvious approach would be to wrap that non-standard function with a function that does expose that standard interface. For example, suppose you want to "promisity" a function whose signature is:
function nonstandardFn(arg, (data, error) => { ... })
You might wrap this as follows:
function standardFn(arg, callback) {
nonstandardFn(arg, (data, error) => {
if (error) {
} else {
}
});
}
And, then you can "promisify" that standardized wrapper with the following:
const wrapped = util.promisify(standardFn);
Creating an instance of class Promise
A different approach is to write a function that explicitly creates
and returns an instance of class Promise
and (2) to make that
instance of class Promise a wrapper around a call to the async
function. Here is an example that wraps the standard async function
fs.readFile
:
function read_file(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, {encoding: 'utf-8'}, ((err, data) => {
if (err) {
reject(`bad path; cannot read file "${path}"`);
} else {
log(`resolved - path: ${path} data.length: ${data.length}`);
resolve(data);
}
log('(read_file) path: ${path} finished');
}));
});
}
And, you would call this function with something like this:
const promise = read_file(fileName);
const data = await promise;
Or, more directly, call it as follows:
const data = await read_file(fileName);
Here is a more complete example of calling our function that creates
and returns an instance of Promise
:
async function test(paths) {
// Call `read_file` for each path to produce an array of promises.
const promises = paths.map(async (path) => {
return await read_file(path);
});
const results = await Promise.all(promises);
log(`results.length: ${results.length}`);
const separator01 = '-'.repeat(50);
results.forEach((data, idx) => {
log(separator01);
log(`path: ${paths[idx]}`);
log('-'.repeat(paths[idx].length + 8));
log(data);
});
}
Explanation — With a little inspection, I think you will see the
general approach and strategy here: Create an instance of class
Promise
and in the callback function passed to that constructor,
call your async function and then call the resolve
method when you
have and want to return the successful result, but call the reject
method if something goes so wrong that you want to abort.
Generating a wrapper that returns a promise
Now we'd like to know how to write a function that takes an async
function as its argument and returns a function that creates and
returns a Promise
for calling that function. In effect, we want a
function that produces the example function read_file
above. And,
another way of saying this is that we'd like to be able to write
custom versions of util.promisify
ourselves. In particular,
whereas util.promisity
works for functions that the
follow the common error-first callback style, i.e. that take an
(err, value) => ... callback
as the last argument, and returns a
version that returns promises, in contrast, we'd like to implement a
"promisity" function that hands cases that do not follow that
common callback style.
Actually, there are two general approaches to doing this.
Let's consider one of the simpler cases, first. Suppose you'd like
to use util.promisify
to wrap a callback function, and the only
thing that's preventing you is that the order of parameters of the
function you want to wrap is non-standard. Then it's usually
trivial to write a function that takes the more standard order of
parameters required by util.promisify
and calls your function.
Consider a example.
Let's wrap the function setTimeout
:
> function setTimeoutAdapter(milliseconds, callback) {
> setTimeout(callback, milliseconds);
> }
And, then we can "promisify" it as follows:
> promiseFunc = util.promisify(test.setTimeoutAdapter)
Finally, we can call our promisified function. If will return an
instance of Promise
, on which we can await
:
> await promiseFunc(3000); console.log('finished after 3 seconds');
finished after 3 seconds
But, what about more complex situations? For some of those needs,
at least, util.promisify
provides a customizable solution.
If you read further in the documentation for util.promisity
, you
will find instructions on how to do this. Look for
Custom promisified functions.
Here is a more complete example of the use of promisify.custom
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
|
Notes:
-
Note 1 —
delay
is the function with the non-standard signature that we want to "promisigy". -
Note 2 — Here we override the return value of
util.promisify()
. -
Note 3 — Here we create our "promisified" version of a delay function that uses
setTimeout
. -
Note 4 — And this is a test of our "promisified" function.
More information — You can learn more about customized use of
util.Promisify
here
A Complete Guide - NodeJS Using Promisify and Utility Functions