Mitigating JavaScript Promise Pitfalls for Occasional JavaScript Developers

Helping the Occasional JavaScript Developer with Tracked Promises

I am currently working with Promises 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.

Promises 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 Promises 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 Promises 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 Promises 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 Promises 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!

https://github.com/zwettemaan/CRDT_UXP/blob/7c081ca37c544f5c578005e3feb0552a05767155/CreativeDeveloperTools_UXP/crdtuxp.js#L4233

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 of TrackedPromise.
  • 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.