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 use await 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
#!/usr/bin/env node

const { Command } = await(import('commander'));
const process = await import('node:process');
const { promisify } = await import('node:util');
const log = console.log;

// See Note 1
// Stall or wait for n seconds.
function delay(seconds) {
  return new Promise((resolve, /* reject */) => {
    setTimeout(() => {
      resolve(`done after ${seconds} seconds`);
    }, seconds * 1000);
  });
}

async function testDelay(seconds) {
  const doDelay = function() {};
  // See Note 2
  doDelay[promisify.custom] = () => {
    return delay(seconds);
  };
  // See Note 3
  const promisifiedDelay = promisify(doDelay);
  log('before');
  // See Note 4
  await promisifiedDelay();
  log('after');
}

function test(opts, args) {
  if (opts.delay) {
    if (args.length != 1) {
      log('need argument delay (seconds)');
      process.exit();
    }
    const seconds = Number(args[0]);
    testDelay(seconds);
  }
}

async function main() {
  const program = new Command();
  program
    .argument('<arguments...>')
    .option('-d, --delay', 'create delay promisify function', false, )
  ;
  program.parse();
  const opts = program.opts();
  const args = program.args;
  await test(opts, args);
}

export { test, testDelay, };
if (process.argv[1] === import.meta.filename) {
  await main();
}

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


Published

Category

Nodejs

Tags

Contact