Helping the Occasional JavaScript Developer with Tracked Promises
I am currently working with Promise
s in JavaScript and exploring ways to make them more digestible for an Occasional JavaScript Developer (OJD).
Because I routinely dive in and out of various development environments, I consider myself an OJD; after I’ve done PHP or Python or C++ or ExtendScript all day, switching to a JavaScript mindset doesn’t come easy.
Surprises, and not the good kind
Much of the code I write is used by other OJD (or OC++D or OPHPD…), and while coding, I strive to avoid surprises.
By avoiding surprises I mean: if you read some source code first, and then step it with a debugger, everything works as you’d expect. It should be outright boring.
I find that it is fairly hard to write JS code without surprises; you don’t need much for the code to suddenly veer off on an unexpected tangent.
I prefer my code to be gracious, spacious, and clear. A calm, open landscape rather than a dense, compressed knot of complexity waiting to unravel.
Limited Lifetime Scripts
In some JavaScript environments (e.g. UXP/UXPScript), scripts have a limited runtime/lifetime.
Any unsettled promises disappear and will never be handled if the script terminates before the JavaScript runtime had a chance to settle all promises.
Promise
s and async
/await
are great when you live and breathe JavaScript day in day out, but the OJD will often find themselves on treacherous terrain.
One issue with Promise
s is that it is very easy to forget to wait for the Promise
to settle (or with async
/await
, forgetting an await
), and then these Promise
s might silently disappear, never to be settled.
Using VSCode and Copilot and other stuff helps, but I still don’t like it.
Fire and Forget
What compounds the issue is that I like to use a ‘Fire and Forget’ approach for certain methods, for example, logging to a file.
If the logging module I am using is asynchronous, it makes sense to me to just call the logging calls, and not await
or then
them.
My logging strings are time-stamped anyway, and I don’t really care when a log entry makes it into a log file. I want to fire off the logging call, and carry on with the ‘meat’ of the code, full speed ahead.
The issue is that if the script terminates too early, those pending logging calls never reach the log file.
There was a lot of head scratching during my initial experiments with UXPScript for InDesign and Photoshop, until I realized what was happening to my logging output.
Fixing the issue
There are multiple approaches to work around this (such as: keeping a list of all pending log calls and await them later with a combined Promise
, using event emitters, relying on assistance from VSCode, Copilot and other tools…).
But nearly all of these approaches seem to be quite cumbersome for the OJD.
Tracked Promises
I developed an approach that seems to give me the lowest possible impact on the source code.
It’s not a panacea – it involves replacing the built-in Promise
class with a subclass thereof, which can have negative consequences.
However, in the specific environments (UXP/UXPScript) where I am using them I feel my ‘hack’ makes it a little easier for an OJD to grok my code and maintain it without needing to fully understand async JS.
What I am doing is subclassing Promise
with a new subclass (for the sake of argument, let’s call it TrackedPromise
), and then assign TrackedPromise
to global.Promise
.
This causes the remainder of the script to use the TrackedPromise
rather than the ‘regular’ Promise
. The UXP/UXPScript engine will also automatically use the TrackedPromise
for async
/await
.
The TrackedPromise
is identical to Promise
, except that it tracks construction and settling of Promise
s and it retains a dynamic collection of ‘unsettled promises’. Promises that settle are removed from the collection – only unsettled promises are tracked.
When my script is about to end (i.e., ‘fall off the edge’), I call a static method on the TrackedPromise
to await any unsettled promises before proceeding.
That way, I can make sure Promise
s don’t disappear into the bit bucket and my ‘fire and forget’ logging calls all make it.
This also helps me finding any forgotten await
or then
; I can easily add debugging code to the TrackedPromise
to point out where the issues are.
If you’re interested to see some code, have a look at the Github source code for Creative Developer Tools for UXP, link below. The tracked promises are part of the crdtuxp
runtime.
Note: this code is work-in-progress. Also, use this idea at your own peril! While it works for me, there are no warranties, expressed or implied!
Potential Concerns
- Overwriting
global.Promise
can lead to unintended consequences, especially in larger or more complex applications. Other libraries or parts of the code might not expect the modified behavior ofTrackedPromise
. - Introducing a custom
Promise
subclass adds a layer of abstraction. If this is unexpected, it can make debugging more ‘surprising’, rather than less. - Tracking all unsettled promises and waiting for them to settle at the end of the script can introduce a non-negligible performance overhead.
- Future updates to the JavaScript engine could inadvertently introduce bugs or incompatibilities with this approach.
If you find this helpful, make sure to give me a positive reaction on LinkedIn! I don’t use any other social media platforms. My LinkedIn account is here: