npm / node-which

Like which(1) unix command. Find the first instance of an executable in the PATH.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[BUG] Callback is part of promise chain

vweevers opened this issue · comments

What / Why

The promise support introduced in 2.0.1 has a side-effect on the callback interface: the callback becomes part of the promise chain. If the callback throws, an unhandled promise rejection follows. With node's current default behavior (of not crashing) this is problematic.

When

Using which >= 2.0.1 with the callback interface.

How

Current Behavior

return cb ? step(0).then(res => cb(null, res), cb) : step(0)

Steps to Reproduce

which('foo', function () {
  throw new Error('Something went wrong outside of the domain of `which`')
})

Expected Behavior

There are two possible solutions:

  1. Call the callback in a next tick, to escape the promise chain
  2. Don't create a promise if a callback is passed in; instead follow this pattern:
function example (callback) {
  if (!callback) {
    var promise = function (resolve, reject) {
      callback = function (err, res) {
        if (err) reject(err)
        else resolve(res)
      }
    }
  }

  // ..

  return promise
}

Would you take a PR for either?

So something like this, then?

diff --git a/which.js b/which.js
index 82afffd..1aa94c1 100644
--- a/which.js
+++ b/which.js
@@ -40,6 +40,8 @@ const getPathInfo = (cmd, opt) => {
   }
 }
 
+const escapePromise = fn => (...args) => process.nextTick(() => fn(...args))
+
 const which = (cmd, opt, cb) => {
   if (typeof opt === 'function') {
     cb = opt
@@ -48,6 +50,9 @@ const which = (cmd, opt, cb) => {
   if (!opt)
     opt = {}
 
+  if (cb)
+    cb = escapePromise(cb)
+
   const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt)
   const found = []
 

Not creating a promise when a cb is passed in would be difficult, because we're using resolve/reject to tie the step/substep calls together, so it either resolves with a value or another promise.

Anyway, yeah, nextTick is fine. Just add a test to that patch, and I'll accept it. :)