8

Environment: node 8.11.x I want use async/await for sleep a while.

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
await sleep(5000)

This works.

const sleep = util.promisify(setTimeout)
await sleep(5000)

It cause a exception: TypeError: "callback" argument must be a function. setTimeout The document note: This method has a custom variant for promises that is available using util.promisify()

So what's the difference?

xingfinal
  • 131
  • 1
  • 1
  • 4
  • 1
    that's very interesting about the custom variant - you know what's interesting about it? the code you says "cause an exception" ... **doesn't** - it works - in node 10.8.0 - so perhaps there's a bug in 8.11.x (whatever that is) – Jaromanda X Aug 12 '18 at 00:03

4 Answers4

13

promisify is expects a function that has final argument that is the callback.

In other a words it wants a function that looks like:

function takesACallback(str, Fn) {
    Fn(null, "got: ", str)
    // or with an error:
    // Fn(error)
}

Of course setTimout is the opposite. The argument you want to pass in is the last argument. So when you try to call the promisifyd function and pass in an argument, it will take that argument -- the delay-- and try to call it like a function. Of course that's an error.

For entertainment (and slight educational) purposes only, you can pass in a function that reverses the arguments and it will work:

let util = require('util')

let pause = util.promisify((a, f) => setTimeout(f, a))
pause(2000)
.then(() => console.log("done"))

Now the final argument of the function you passed to promisify expects function. But the asyn/await method is so much nicer…

Mark
  • 90,562
  • 7
  • 108
  • 148
  • 7
    I am entertained - and educated! :p – Jaromanda X Aug 11 '18 at 03:23
  • You say `But the asyn/await method is so much nicer` - but that still requires a promisified timeout, doesn't it? – Jaromanda X Aug 11 '18 at 03:24
  • Yes, you still need to create the promise, but I think `util.promisify` with the reversed arguments is very confusing. Maybe **so** much nicer was a bit of an overstatement. – Mark Aug 11 '18 at 03:26
  • `let p = util.promisify(setTimeout.bind(null, cb=>cb()));` seems to work :p (for more amusement and education) – Jaromanda X Aug 11 '18 at 03:29
  • how do you think I felt writing it :p – Jaromanda X Aug 11 '18 at 03:31
  • 6
    [setTimeout](https://nodejs.org/docs/latest-v8.x/api/timers.html#timers_settimeout_callback_delay_args) The document note: This method has a custom variant for promises that is available using util.promisify() – xingfinal Aug 11 '18 at 15:10
8

You know that this right here works:

const {promisify} = require('util');
const sleep = promisify(setTimeout);

;(async () => {

  const ts = Date.now()

  await sleep(5000)

  console.log(Date.now()-ts)

})();

This works fine, why not go with it and use it???

g00dnatur3
  • 1,173
  • 9
  • 16
7

This can be a one-liner: await promisify(setTimeout)(1000).

It works because setTimeout has a custom variant for promisify. It does work with node 8.11.

nvm install 8.11 && nvm use 8.11
node <<HEREDOC
  (async () => {
    // const start = Date.now();
    await require('util').promisify(setTimeout)(5000);
    // console.log(Date.now() - start);
  })()
HEREDOC
Patrick Fisher
  • 7,926
  • 5
  • 35
  • 28
2
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

Is the essence of the implementation of util.promisify(setTimeout), but there are additional features to the promisify implementation:

  • it takes an optional value argument that is returned as the value of the promise
setTimeoutPromisify(10, 'foo bar').then(value => console.log(value)) // logs "foo bar"
console.log(await setTimeoutPromisify(10, 'foo bar')) // logs "foo bar"
  • it takes an (optional) options argument that
    • lets you specify an AbortController signal to cancel the timeout
    • lets you choose whether or not to keep a ref, which if set to false, means the program will exit before the timeout finishes if nothing else is happening
const controller = new AbortController();
const signal = ac.signal;

setTimeoutPromisify(1000, 'foo bar', { signal, ref: true })
  .then(console.log)
  .catch((err) => {
    if (err.name === 'AbortError')
      console.log('The timeout was aborted');
  })

controller.abort()

For more information see https://nodejs.org/api/timers.html

The implementation in node 16 is below:

function setTimeout(after, value, options = {}) {
  const args = value !== undefined ? [value] : value;
  if (options == null || typeof options !== 'object') {
    return PromiseReject(
      new ERR_INVALID_ARG_TYPE(
        'options',
        'Object',
        options));
  }
  const { signal, ref = true } = options;
  try {
    validateAbortSignal(signal, 'options.signal');
  } catch (err) {
    return PromiseReject(err);
  }
  if (typeof ref !== 'boolean') {
    return PromiseReject(
      new ERR_INVALID_ARG_TYPE(
        'options.ref',
        'boolean',
        ref));
  }

  if (signal && signal.aborted) {
    return PromiseReject(new AbortError());
  }
  let oncancel;
  const ret = new Promise((resolve, reject) => {
    const timeout = new Timeout(resolve, after, args, false, true);
    if (!ref) timeout.unref();
    insert(timeout, timeout._idleTimeout);
    if (signal) {
      oncancel = FunctionPrototypeBind(cancelListenerHandler,
                                       timeout, clearTimeout, reject);
      signal.addEventListener('abort', oncancel);
    }
  });
  return oncancel !== undefined ?
    PromisePrototypeFinally(
      ret,
      () => signal.removeEventListener('abort', oncancel)) : ret;
}
Garrett Motzner
  • 3,021
  • 1
  • 13
  • 30