Skip to content
Advertisement

How to detect whether function return value discarded or not?

Use-case: This allows to differ whether the user have used promise-based style or callback style thus I can avoid double computations. I monkey-patch both function and that on-completed setter with ES6 Proxy and now I’m doing expensive computations in both. I want to avoid that. There’s no way knowing whether website have used the Promise-based version or callback-version because by the time website call promise based version, the on-completed setter is empty.

The native function in question didn’t had promise based version in the past.

I can’t use Static Analysis because I’m monkey patching native functions websites use (whose code not under my control)

// 2009

// The function is void in 2009
// Today it can return a promise with a value but websites still can use 
// 'nativeFooOnCompleted' to get result

nativeFooStart();

nativeFooOnCompleted = function() {};

// Today
// "nativeFooStart" is still required and without that on-completed 
// event won't fire

let result = await nativeFooStart();

// or 
nativeFooStart();
nativeFooOnCompleted = function() {};

I need to optimize runtime. My real-life function otherwise will do complex expensive calculations inside the function whether discarded or not. This is something not handled by V8 engine. I’m modifying a native function (Monkey-patching) not even my own function. In my opinion, this is a simple question given the browser APIs allows direct access to source code of the script so one can travel through the code and figure out whether function return value discarded or not.

Here is the code that highlights two function calls, one caller discarded the return value while the other caller didn’t.

function foo() {
  return "bar";
}

foo();  // I need to detect this

let bar = foo();

I need to detect this at Runtime. From my research, I found that Perl has wantarray which will tell you not only if the return value is being assigned.

The rest languages can only do it at compile-time.

I’ve made significant progress since when issue was created. I’ve able to come up with an approach and it’s valid but it missing one thing to consider as a true solution.

   function foo() {
       // Increase Stacktrace Limit
       Error.stackTraceLimit = Infinity;
        
         // Get the stack trace
       let stackTrace = (new Error()).stack.split("n"); 
                     
       // Get the Last Item of Trace and Trim it
       let lastLine = stackTrace.pop().trim();
       
       // Get Index of "at "
       let index = lastLine.indexOf("at ");
       
       // Get Normalized Line
       let normalizedLine = lastLine.slice(index + 2, lastLine.length).trim();
       // Regex Pattern to extract line number
       let lineNumberPatternRegex =  new RegExp(/:(d+):(?:d+)[^d]*$/);
       
       // Get Line Number
       let lineNumber = lineNumberPatternRegex.exec(normalizedLine)[1];
       
       // Get the Source Code
       let sourceCode = document.currentScript.text.split("n");
       
       // Store Caller Line Here
       let callerLine;
       
       // Test whether we have to count HTML lines
       // See, https://stackoverflow.com/q/66388806/14659574
       if(sourceCode.length < lineNumber) {
          // Get HTML Source Code as String
            let HTML = new XMLSerializer().serializeToString(document)
          
          // Get HTML Source Code as Lines
          
          let HTMLSourceLines = HTML.split("n");

            // This part is stuck because Devtools see diff HTML
          // I still yet to figure how to grab that
          // See, https://stackoverflow.com/q/66390056/14659574
       } else {
          callerLine = sourceCode[lineNumber - 1];
       }
       
       // Detect Variable and Object Assignments 
       // Minified cases not yet handled here
       if(callerLine.includes("=") || callerLine.includes(":")) {
            console.log("Not Discarded")
       } else {
          console.log("Discarded")
       }
       
       return "bar"
    }
      
foo();

User @poke answered sub-problem of this problem here Link for Sub Problem

According to him,

serializeToString(document) will serialize the current document state, including all the changes that may have been applied at runtime. In this case, additional styles were added after the page has been rendered but there may be more drastical changes too that completely remove or redorder things.

When you look at the stack trace from JavaScript, then the browser’s JavaScript engine will attempt to give you information that is closely related to the original source since that’s where your code is coming from. If you use source maps with minified code, the browser is usually even able to tell you where a particular thing came from in the original unminified code even if that code does not even closely match the code that is being executed (e.g. when using a transpiler).

In the end, you cannot really figure out what the browser will tell you where the line of code came from just by looking at the document at runtime. If your code follows very strict rules, you may be able to estimate this with some calculations but this isn’t a safe approach.

Advertisement

Answer

Tl;dr Schedule a microtask

The point being that using await schedules the rest of the function as a microtask.

Please note that this answer don’t attempt in any way to detect whether a value has been discarded or not. This is solely in answer to the first paragraph (Use-case), dropping the need for the both static code analysis and run-time source code parsing.

The purpose is just to yield control to the calling routine.

await nonPromiseValue is the same as await Promise.resolve(nonPromiseValue). It completes “instantly” but still schedules the code after the await expression to run later. So with f = async () => { await 1; 2;} and calling f(); g() the code will first reach await 1 -> sleep f and schedule the rest on the microtask queue -> call g() -> (eventually when the microtask queue gets to it) resume f() continuing with 2

The values from which to what it changes, or whether it does at all, do not make difference.

let onCompleted; // This would be a property of some request object but that's not needed for the demo

function takeHoursToCompute() { console.log('computing'); return 'Done'; }

function takeMinutesToProcess() { console.log('processing'); }

async function f() {
  // We want to see whether the caller sets onComplete. The problem is that it happens only after calling f().
  // However, if we await it (even though it's not a Promise), this will let the calling function finish before continuing here.
  // Note that *at this point* await(onCompleted) would give undefined, but...
  await onCompleted;
  //...but *by now* onCompleted is already what the caller had set.
  
  const result = takeHoursToCompute();
  if(typeof onCompleted === 'function') {
    // non-Promised call
    takeMinutesToProcess();
    onCompleted(result);
  } else
    console.log('skipping processing');
  
  return result; // returns Promise (because async)
}

window.addEventListener('DOMContentLoaded', () => { // calling code
/* Don't call both branches, because I don't clear onComplete anywhere and that would break the purpose. */
if(true) {

// callback style
  f();
  onCompleted = result => document.getElementById('result').textContent = result;

} else {

  // async style
  (async() => {
    document.getElementById('result').textContent = await f();
  })();

}
});
Result: <span id="result"></span>
<br>
See the console too!

Credits: @TheVee & @Vlaz

User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement