While using javascript I keep running into situations, where previously synchronous code suddenly requires a value, that can only be obtained asynchronously.
For instance, I am working on a TamperMonkey script, where I have a function that operates on a string parsed from location.hash
. Now I want to change the code to enable persistence across URL changes in a tab, by using the GM_getTab(callback)
interface.
Since I need a couple of functions to be executed in unchanged order, a knock-on effect occurs, since I need to await
the value and I suddenly end up refactoring several functions along the call-stack into async
functions, until I reach a point, where the order needs no longer be guaranteed.
More importantly however, promises needing to be explicitly await
ed, can lead to unexpected behavior, when an await is forgotten: E.g. if(condition())
may suddenly always evaluate to true
, and 'Hello '+getUserName()
may suddenly result in Hello [object Promise]
.
Is there some way to avoid this "refactoring hell"?
Simplified example
Subsequently, I present a heavily simplified example of what happened: Needing an await
down the callstack, while needing to preserve the execution order lead to refactoring all the way up to the event callback updateFromHash
.
// -- Before
function updateFromHash(){
updateTextFromHash();
updateTitleFromText();
}
function updateTextFromHash(){
DISPLAY_AREA.innerText = getTextFromHash();
}
// -- After
async function updateFromHash(){
await updateTextFromHash();
updateTitleFromText();
}
async function updateTextFromHash(){
DISPLAY_AREA.innerText = getTextFromHash()
|| await new Promise((accept,reject)=>GM_getTab(accept));
}
In this case it is relatively simple, but I have previously seen the asynchronicity bubble up much farther in the call stack and cause unexpected behaviour, when an await
was missed. The worst case I've seen was, when I suddenly needed the "DEBUG" flag to depend on an asynchronously stored user setting.
Time Constraints
As pointed out by @DavidSampson, it would probably have been better in the example for the functions not to depend on mutable global state in the first place.
However, in practice code is written under time constraints, often by other people, and then you need a "small change" – but if that small change involves asynchrnous data in a formerly synchronous function, it would be desirable to minimize the refactoring effort. The solution needs to work now, cleaning up the design problem may have to wait until the next project meeting.
In the example given, refactoring is feasible, since it is a small, private TamperMonkey script. It ultimately only serves to illustrate the problem, but under commercial-project scenarios, cleaning up the code may not be feasible within a given project scope.