node.jsnode-promisify

Promisifying custom functions with util.promisify


I would like to use util.promisify to promisify custom functions. However, util.promisify works for node-style methods that have an error-first callback as the last argument. How can I adjust my custom functions so that they will work with util.promisify?


Solution

  • How can I adjust my custom functions so that they will work with util.promisify?

    My first choice would be to just add a new API that returns a promise - that wraps your own API inside a new Promise constructor. Then, you have a documented, promise-returning API that is obvious how to use and requires no extra steps by the client to use it.

    But, if you really want to make something compatible with util.promisify(), read on...

    If you had shown the calling convention for your function that you want util.promisify() to work with, we could have offered a custom implementation specifically for that code, but since you didn't here's an example implementation below.

    The general concept is described here in the doc, though I would not exactly say the doc is very clear about how things work. Here's an example.

    Suppose you have a timer function that takes three arguments in this order:

    timer(callback, t, v)
    

    where t is the time in ms for the timer and v is the value it will pass to the callback when the timer fires. And, it passes the callback two values callback(v, err) in that order. And, yes, this particular timer can sometimes communicate back errors.

    Note, specifically that the callback is passed as the first argument and the callback itself gets err as the second argument, both of which are obviously incompatible with the default implementation of util.promisify() (for purposes of this demonstration) as they don't match the nodejs asynchronous calling convention.

    Here's an example usage of this timer (non-promisified) function that does NOT follow the nodejs calling convention:

    // example
    timer((v, err) => {
        console.log(v, err);    // outputs "hi", null
    }, 2000, "hi");
    

    To make it compatible with util.promisify(), we can create a custom promisify behavior like this:

    // define promise returning implementation of timer that leaves out
    // the callback argument, but takes the same other arguments
    timer[util.promisify.custom] = (t, v) => {
        return new Promise((resolve, reject) => {
            timer((value, err) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(value);
                }
            }, t, v);
        });
    };
    

    The general concept is that when you pass a function reference to util.promisify(fn), it looks for fn[util.promisify.custom] and, if it exists, it will use that promisify implementation instead of its default one. If you're wondering what util.promisify.custom is, it's a symbol defined by the util library - just a unique property name that you can assign to your own function as a property in order to identify that you have implemented a custom promisify capability. Since symbols are unique within any nodejs implementation, adding that property to your function can't interfere with any other properties.

    Which can then be used like this by other callers:

    const timerP = util.promisify(timer);
    
    // you call timerP, leaving out the callback argument   
    timerP(3000, "bye").then(result => {
        console.log(result);           // outputs "bye" after 3000ms
    }).catch(err => {
        console.log(err);
    });
    

    If you want to see the nodejs implementation for util.promisify(), you can see it here in the actual util.js code.