Context
I’m currently writing a Node.js package for a wrapper I’m calling a curfew-promise. It’s simpler than I thought, but it is still worth doing in my opinion. The package exports a single function that returns a promise. The promise has “3” parameters with the function header being: (curfew, func, ...args)
. The promise then performs func
asynchronously (so func
itself can be sync or async). It passes ...args
to that function when it’s called. Lastly, the key idea here is that if func
takes longer than curfew
milliseconds to complete, the wrapper will become a rejected promise.
At first, it seemed like a tough task, until I discovered the built in Promise.race()
function which creates a promise which we can refer to as the racePromise
. That function then also takes in multiple other promises. Whichever passed in promise resolves or rejects first, its value is then passed onto racePromise
. I can achieve my curfewPromise
then by creating one promise that runs func
and one that rejects after curfew
has passed. Whichever finishes first becomes the value of the promise the function returned. The only way I’ve found to pause an async operation in JavaScript is via await new Promise((resolve) => { setTimeout(resolve, duration); });
This line, when placed into any async function, will pause operation until duration
has passed. You can also remove the redundant brackets and shrink it down to await new Promise(r => setTimeout(r, duration));
, but I prefer to be more consistent.
JavaScript is NOT Multi-Threaded
JavaScript runs in a single thread. The way it pretends to be multi-threaded is by switching back and fourth between tasks to give all of them a little bit of time until they all finish. JavaScript is also an interpreted language. This means while trying to test my package, I had a lot of trouble. I spent a few hours trying to understand what was going on and now that I have, I’m going to write about it to save you trouble.
The Code
I was completely convinced that JavaScript itself was just broken. Here is my package, at least in its current state: (If you’d like to use this or see the modern version, check the GitHub page (soon to also be on npmjs.com))
module.exports = (curfew, func, ...args) => { let waitId; async function wait() { await new Promise((resolve) => { waitId = setTimeout(resolve, curfew); }); return Promise.reject(new Error("Curfew of " + curfew + "ms have elapsed.")); } async function perform() { const value = await func(...args); if(waitId) clearTimeout(waitId); return Promise.resolve(value); } return Promise.race([wait(), perform()]); }
wait()
creates a promise (using async notation, since from what I’ve read that is more ideal than direct promise notation) which waits until curfew
has passed, which then rejects with a useful stack trace message. You’ll notice I’m also storing waitId
, which is the ID of the setTimeout
call. This way, if func
finishes before curfew, I can cancel the timeout and not waste performance. I’ll also be looking into ways to create cancellable promises. I’m aware there are already packages that do this, but I think I’ll benefit from doing it by hand. I could make wait()
a synchronous function that simply returns a waiting promise that calls setTimeout
for reject
, but I chose making it like this because then it matches the form of perform()
(two async functions), and it allows me to write two lines that are visibly neat rather than trying to force everything into one line.
perform()
creates a promise that waits until func
is finished. Once it is, it’ll attempt to stop the waiting promise. If the promise is still waiting, it’ll stop it. If the promise has already rejected, it will do nothing. Then, the function returns a resolved promise with the value from func
. I chose to write return Promise.resolve(value)
instead of return value
, which I understand to be the same thing, for consistency once again, and I think it makes the code more readable overall.
Lastly, I am creating the promises off of these functions and making them race. The function returns a promise that resolves or rejects whenever the first of the two – wait()
and perform()
– finish.
The Problem
I was able to narrow down the problem to this line:
const value = await func(...args);
All of my tests had worked until I did something like the following:
const curfewPromise = require("./index"); // Broken Promise 1 console.log(curfewPromise(10000, async () => { for(i = 0; i < 1000000; i += .001); })); // Broken Promise 2 console.log(curfewPromise(10000, async () => { while(true); }));
I found that for broken promise 1, despite the fact that curfewPromise returns a promise, it wouldn’t finish until the for loop finished. Even if I set the curfew to 0, it would take until the for loop completed. Broken promise 2 would never even end. I purposefully wanted to test these edge cases, where something would actually take a long time for ever (a good example for why I want the ability to cancel a promise).
The problem that I realized so frustratingly last evening is, and say it with me, JavaScript is NOT multi-threaded. Yes, when you run an asynchronous function, you can do other things in the meantime while it finishes. But as far as I can tell, JavaScript does this on a line by line basis. If you have a single line that is very slow or infinite, JavaScript will start running it, as that’s how asynchronous functions work, and then after some progress, it will move on. The thing is, you can’t make progress until that line ends. In the case of an infinite while loop, it will never end. In the case of a very slow one-line for loop, it can’t move on until the for loop is finished. Promise.race()
cannot return a promise, so curfewPromise
cannot return a promise. Once the for loop actually ends, it’s a gamble to decide which non-neutral-state promise will be chosen to have won the race. (Also keep in mind when using very small curfews
, there is internal delay and it may not behave how you expect).
Conclusion
JavaScript is not multi-threaded. Make sure you keep that in the back of your mind. I was inadvertently testing for a case that shouldn’t ever even happen. While it was frustrating trying to figure out what was wrong, I still think that this kind of struggle is necessary in both life and learning. I always learn the most in coding when I’m trying to find out how to do something and I have 20 tabs open.
While this is a simple package that does something many developers probably can do easily, I still want to spend time making it and even upload it to npm. I think even though it’s simple, the function still helps clean up syntax to improve readability and reduce lines. I also think making it an external package helps to reduce complexity in your projects and helps me use it in multiple projects. Maybe someone can also look at it for help when learning how promises work.
At the end of the day, promises and asynchronous functions are an incredibly valuable tool in JavaScript and the illusion of multiple threads helps in most situations. Just be careful to not break your promises.
From the blog CS@Worcester – The Introspective Thinker by David MacDonald and used with permission of the author. All other rights reserved by the author.