rogierschouten/async-lock

Unhandled promise rejection on lock timeout

vincentwoo opened this issue · 2 comments

Examine the following code:

const AsyncLock = require("async-lock");
const lock = new AsyncLock({ timeout: 5000 });

lock.acquire("test", function(done) {}); // hold the lock open
const promise = lock.acquire("test", function() {}); // try to acquire the busy lock
promise.then(() => console.log("success"));
promise.catch(() => console.log("fail"));

What do you expect the program to output? I was expecting "fail", but the reality was:

$ node test.js
fail
(node:20581) UnhandledPromiseRejectionWarning: Error: async-lock timed out
    at Timeout._onTimeout (/home/vwoo/shared/autocomplete/node_modules/async-lock/lib/index.js:164:17)
    at ontimeout (timers.js:436:11)
    at tryOnTimeout (timers.js:300:5)
    at listOnTimeout (timers.js:263:5)
    at Timer.processTimers (timers.js:223:10)
(node:20581) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:20581) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

It is very strange to me that we have both the "fail" message as well as an unhandled promise rejection error. I'm not sure why and I'll try to take a peek inside the library to understand (just documenting my findings here).

One thing that jumps out at me is that the library seems to have two different ideas of when it should be in "promise" mode or not.

/**
 * Acquire Locks
 *
 * @param {String|Array} key 	resource key or keys to lock
 * @param {function} fn 	async function
 * @param {function} cb 	callback function, otherwise will return a promise
 * @param {Object} opts 	options
 */

//... 

	if (typeof (cb) !== 'function') {
		opts = cb;
		cb = null;

		// will return a promise
		deferred =  new this.Promise(function(resolve, reject) {
			deferredResolve = resolve;
			deferredReject = reject;
		});
		deferred.catch(function() { console.log('wtf3') })
	}

The jsdoc claims that it is the presence of the cb function that triggers promise mode, which, hey, fair enough. However, elsewhere in the code:

		// Callback mode
		if (fn.length === 1) {
			var called = false;
			fn(function (err, ret) {
				if (!called) {
					called = true;
					done(locked, err, ret);
				}
			});
		}
		else {
			// Promise mode
			self._promiseTry(function () {
				return fn();
			})
			.then(function(ret){
				done(locked, undefined, ret);
			}, function(error){
				done(locked, error);
			});
		}

It seems the library also uses the signal of whether the fn executing function accepts a done callback parameter or not to determine if it is promise mode. This seems like a potential cause of this issue.

Oh, maybe I don't have a bug, I forgot that promises fork in the chain - I should have written

promise.then(() => console.log("success")).catch(...

Closing for now!