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.

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:

https://www.linkedin.com/in/kristiaan

Re: Tidy InDesign Scripts Folder/Stub Scripts

Playing with InDesignBrot and PluginInstaller

If you want to see how those stub scripts work, here’s how you can do that.

You need to have installed PluginInstaller build #583 or higher (https://PluginInstaller.com) you can use the following link to install InDesignBrot_idjs into your Scripts Panel without unwanted clutter.

With PluginInstaller 0.2.4.583 or higher installed, click the following link:

InDesignBrot_idjs for InDesign sample script

PluginInstaller should open.

Click the Download button.

If you have multiple versions of InDesign installed, use the Install Target dropdown menu to pick the one you want to install into.


Click the Install button, and switch over to InDesign. The script should become available on your InDesign Scripts Panel.

Some rules of thumb:
– Doubling max steps will roughly double the time it takes to calculate the image.
– Doubling num pixels will roughly increase the time needed by a factor of four.

Start out with the default values and first gauge how long the script takes on your computer: your mileage may vary, and as far as I can tell my Mac with an M2 Max is pretty darn tootin’ fast.

I also found that on my Mac, InDesign now crashes when I set num pixels to 200, but 149×149 works fine (and takes about 30 mins to calculate).

Not sure about that, maybe the sheer amount of rectangles needed (40,000) is more than InDesign can handle. But I’ve calculated 200×200 renditions with earlier versions of InDesign, and those files still open just fine.

More Info About InDesignBrot

InDesignBrot source code is available on Github. I have a separate branch for a version based around Creative Developer Tools for UXP (CRDT_UXP). Note the README info further down this page.

https://github.com/zwettemaan/InDesignBrot/tree/CRDT_UXP

The main branch on Github is for an older, hybrid version where the same source code can be run in ExtendScript as well as UXPScript, for speed comparisons:

https://github.com/zwettemaan/InDesignBrot

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:

https://www.linkedin.com/in/kristiaan

InDesign UXPScript Speed

Or, “how a single comment line can make an InDesign UXPScript run more than five times slower”.

The Issue

I discovered a weird anomaly in InDesign UXP Scripting which can adversely affect the execution speed of a UXPScript.

I also tried it out with Photoshop. As far as I can tell, Photoshop UXPScript is not affected by this.

Simply adding a comment line like

// async whatever

into the ‘main’ .idjs file makes my script way, way slower.

A noticeable difference is a different redraw behavior while the script is executing.

I suspect the InDesign internal UXP Scripting module performs some crude preliminary textual scan of the script source code before launching the script, and InDesign behaves differently depending on whether it found certain keyword patterns or not.

The textual scan does not seem to care where the patterns occur: e.g. in comments, or in strings or in actual source code.

The issue does not occur for anything that appears in a submodules (require). I am guessing the preliminary textual scan only inspects the ‘top level’ .idjs script.

Because this textual scan does not account for patterns occurring in comments, I can simply add a dummy comment line with the right pattern and trigger the behavior, and make my script become much slower.

It took me a fair amount of time to figure this out, because the same behavior also occurs when you run the script from the Adobe UXP Developer Tools.

Because there were two unrelated causes for the same symptom, I had to resort to tricks to avoid the ‘Heisenberg effect’.

Initially, each time I tried to observe/debug it, the issue was always ‘there’. And it sometimes did and sometimes did not happen when I ran my script from the Scripts Panel. I tell you, there was much growling and gnashing of teeth.

Demo

I have a benchmarking script, called InDesignBrot, which I keep handy and occasionally use for speed-testing InDesign. I have both ExtendScript and UXPScript variants of the script.

While trying to figure out what was going on, and to help make the issue stand out, I’ve re-worked the UXPScript variant of the InDesignBrot script so it only using Promises. It does not use the async or await keywords at all.

If you run this script from the InDesign Scripts panel, it will calculate a rough visualization of the Mandelbrot set in InDesign, using an NxN grid of square frames.

You can then tweak the parameters on the pasteboard and re-run the script.

On my M2 Max MacBook Pro, the script executes in about 0.5 seconds for a 19×19 grid.

While the script is running, the screen will not update, and the script does not redraw the page until it has completed the calculation.

Then I add a single comment line with the word async followed by a space and another word, like

// async whatever

anywhere in the InDesignBrot.idjs script.

This innocuous change makes the redraw behavior change, and I can now see individual frames being filled, despite InDesign being set to

app.scriptPreferences.enableRedraw = false;

In the end, the same script will take around 3 seconds or more to execute.

The InDesignBrot script can be reconfigured by way of a text frame on the pasteboard. If I change the num pixels to 29, the times become 1 second vs 20 seconds.

If you’re interested in trying this out for yourself, I’ve made a specific branch in the InDesignBrot Github repo. This branch has been trimmed down to remove stuff that’s not relevant to the discussion.

https://github.com/zwettemaan/InDesignBrot/tree/Odd_async_demo

Pull the repo or download the repo .zip and move the InDesignBrot folder onto the Scripts Panel.

Then double-click InDesignBrot.idjs to run the script.

You can tweak the settings on the InDesign pasteboard and experiment by re-running the script as many times as desired.

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:

https://www.linkedin.com/in/kristiaan

Tidy InDesign Scripts Folder

When installing scripts for InDesign, I don’t like to see this:

The issue is that the ‘main’ script is actually InDesignBrot.idjs, but it is buried in chaos, and the user would most probably not know what to do. The InDesign Scripts Panel also shows stuff my scripts need, but which has no relevance to the user of my scripts.

Instead, I want to see something like this (some more utilities thrown in for the sake of argument):

Each individual utility script has a subfolder below the User folder, and within those subfolders, the user sees only clickable scripts. No data files, no templates – just a clickable script.

This functionality is now part of the upcoming release of PluginInstaller.

The new release of PluginInstaller allows you, as the developer, to package scripts and their ‘satellite’ files into a single .tpkg file. It works for both .jsx (ExtendScript) and .idjs/.psjs (UXPScript) scripts.

At the user’s end, the user will use PluginInstaller to install the .tpkg file. Rather than creating a whole raft of files in the Scripts Panel folder, PluginInstaller will create just a ‘link’ to any clickable script files.

My first attempt for this feature was to use symlinks, but that approach has some serious drawbacks. If the actual script files are deleted, the symlinks become broken, and things turn to custard when the user double-clicks the broken entry on the Scripts Panel.

I’ve solved this by using ‘stub’ scripts instead of symlinks. The PluginInstaller will generate a small in-between stub script. When double-clicked, all this stub script will do is transfer control to the actual script, stored somewhere else, in a safe place.

If and when the actual script file is accidentally deleted, the stub script will bring up an error dialog with a clear explanation to the user (essentially: “This link is broken, please use PluginInstaller to re-install the missing script”).

When the user uses PluginInstaller to uninstall a script, the stub script will automatically be removed from the Scripts Panel.

Much cleaner!

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:

https://www.linkedin.com/in/kristiaan