How to detect async function support without eval?
Asked Answered
A

3

21

Here's one way to detect if the current engine supports async functions:

const supportsAsyncFunctions = (() => {
  try {
    new Function('async () => {}')();
  } catch (error) {
    return false;
  }

  return true;
})();

But is there a way to do it without using eval or Function?

Advertise answered 19/4, 2017 at 17:32 Comment(8)
Don't think so, any reason for wanting it?Yila
I think you could probably rewrite your question title to be something along the lines of "How to detect new syntax features without eval". This doesn't limit itself to just async; it also affects checking for arrow shortcuts etc.Osmund
What were you planning to do, or not do, if async/await were or were not available?Reputation
@Osmund there's probably no general way to solve that wider problem, but there might be a specific trick for detecting certain features, e.g. checking for a certain property that only exists on engines that also support async functions. I dunno.Advertise
@torazaburo one option would be choosing whether to load a version of the app that uses async functions vs. a different version that is compiled down to ES5 for older browsers. (Better to use real async functions where possible, for performance, bundle size, and easier debugging of runtime exceptions.)Advertise
The pattern that you have there is the general way to detect such features.Yila
I did some research and, as it was already mentioned, that's the correct way of checking such things, thru eval. The AsyncFunction constructor isn't available straight away: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… You need to create an async function first and then extract the constructor from it. Also such words like 'await' or 'async' aren't protected or reserved, so you can even do const await=1 or let async=2Counterman
The other drawback of eval() and new Function(...) is that both don't necessarily work in conjunction with CSP, notably if the unsafe-eval target is missing from the CSP header. In this case the script is going to throw an error whenever either of the two is encountered, and if it's trapped by a try ... catch, you are going to get a false negative.Palacios
C
13

Suggested eval way will give false negatives for CSP errors because they are not handled. If this is a problem, CSP errors can be handled as shown in this answer.

It is possible to do that, but the solution is not pretty at all and involves external script. The script can set a flag, or global error handler can be set up to watch for syntax errors when the script is loaded.

If the script results in syntax error, it is still considered loaded, so network problems cannot cause false positives:

async-detection.js

window.isAsyncAvailable = true;
async () => {};

main.js

new Promise(function (resolve, reject) {
  var script = document.createElement('script');
  document.body.appendChild(script);
  script.onload = resolve.bind(null, true);
  script.onerror = reject;
  script.async = true;
  script.src = 'async-detection.js';
})
.then(function () {
  console.log(window.isAsyncAvailable);
})
.catch(/* generic script loading error */);

This method can be used to detect syntactic features used on the website that cannot be polyfilled or normally caught with try..catch. It is particularly useful to reliably trigger Your browser is outdated nag screen.

Carpus answered 9/9, 2017 at 5:17 Comment(5)
Not pretty, but very cleverAdvertise
Ha! create and load a an async function to populate a global property for "async is supported." Not sure what to say.Guile
@TonyChiboucas Yes, eval and this one (and their variations) are the only ways to do this, to my knowledge. Script happens.Carpus
CSP = Content Security PolicySamuelsamuela
Great idea. With a bit of tinkering you can even get rid of both the additional script and the indirection via a global property.Palacios
S
2

Extending upon the brilliant ideas in @Estus Flask's answer...

var div = document.createElement("div");
try {
    div.innerHTML = '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" onload="async () => {}; window.isAsyncAvailable = true;">'
} catch (e) {};

This creates a DIV in memory, then appends an IMG within it that uses a Base64 Encoded Pixel while also leveraging this trick to run the eval so that you don't need to define the external script nor have an external image to load. This can also be simplified into a one-liner:

try { document.createElement("div").innerHTML = '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" onload="async () => {}; window.isAsyncAvailable = true;">' } catch (e) {};

However, this runs afoul of some CSP configurations. If this pertains to you, then @Estus Flask's external script is your go-to.

Thanks to @Robidu 's observation, try/catch's added to (hopefully) keep XHTML from barfing.

Note: A try/catch'ed or otherwise unset window.isAsyncAvailable will be falsey and therefore hold the intended value.

Samuelsamuela answered 26/6, 2020 at 9:18 Comment(1)
Another problem with this solution is that it won't work in conjunction with XHTML as assigning a value to innerHTML is going to throw an error.Palacios
P
1

Extending on Estus Flask's answer, I created a variant that doesn't rely on an external script, and it also spares you the indirection of a global property.
Since you don't need a Promise, either, you can run this check as soon as possible when loading a page (even as soon as the <head> section of the document) to facilitate early feature detection.

You simply create a script, assign the script text to it, then attempt to invoke the async function to get a result.

The solution looks like this (updated on Mar 21, 2024):

var this_script = document.currentScript;    // Required for the error suppression...

const prerequisite = -2;        // Prerequisite of feature not available!
const unprobed = -1;            // Feature not probed so far...
const unavailable = 0;          // Feature disallowed or not present
const available = 1;            // Feature natively supported
const emulated = 2;             // Feature provided by emulation

var features = { 'async-functions': unprobed };
window.ns_test = { };    // Serves as a namespace for our tests

// This helps prevent a potential error message from the test to pollute the
// browser's message console with an essentially spurious error message.
var errfunc = function (p_event)
  {
  if(document.currentScript == this_script)
    return;
  if(p_event instanceof SyntaxError)
    p_event.preventDefault();
  };

// This function wraps the handling of the test script up. Declaring it in
// this fashion allows for arbitrary syntactic checks to be performed without
// having to write the entire thing over and over again.
function check_syntactic_feature(p_aux_script, p_feature, p_inverse_logic)
// p_aux_script: The scriptlet to be executed
// p_feature: The feature ID to be set
// p_inverse_logic: Set to true if successfully executing the scriptlet is to be taken as an error
  {
var l_aux_script;
var l_testscript;

  if(typeof p_inverse_logic == 'undefined')
    p_inverse_logic = false;

  l_testscript = document.createElement('script');
// You can mark the spot where to set the feature ID with an %s in the scriptlet string...
  l_aux_script = p_aux_script.replace(/\%s/, p_feature);
  l_testscript.type = 'application/javascript';
// Setting the innerText of the script node neatly bypasses any CSP settings
// concerning eval() so that won't interfere with our test.
  l_testscript.innerText = l_aux_script;
// Adding the test script to the DOM immediately halts execution of this
// script, runs the injected scriptlet and then resumes here.
  document.head.appendChild(l_testscript);
  try
    {
// The function must be added to our namespace so we can get rid of the
// property. If the function is declared directly in the global namespace,
// deleting it doesn't work for some reason.
    ns_test.has_feature(features);
    }
  catch(p_err)
    {
    features[p_feature] = p_inverse_logic ? available : unavailable;
    }
  l_testscript.remove();    // Allows the script to be GCed...
  }

// Since we are checking syntactic features of JavaScript, prevent any
// exceptions from our tests to be written to the console.
window.addEventListener('error', errfunc, false);

// This call invokes our test. Using this function call, more checks on
// syntactic features can be added in an easy way.
check_syntactic_feature('ns_test.has_feature = async function (p_param) { p_param["%s"] = ' + available + '; }', 'async-functions');

// Get rid of some things to permit GCing them...
window.removeEventListener('error', errfunc, false);
errfunc = null;
this_script = null;

// Get rid of the namespace that we have created for our tests to avoid
// pollution of the global object (plus everything referenced in there
// becomes eligible for GCing as well)...
delete window.ns_test;

The argument for the async function has to be an object, because it is passed to a function by reference. Therefore setting any value in the passed object has it elegantly returned to the caller.
The try ... catch statement is again necessary to trap the resulting ReferenceError in case the injected script fails to parse - which would happen if the async keyword is unknown.

CAVEAT

CORS can interfere with the probe in its current form if unsafe-inline isn't set for script-src-elem (or script-src if script-src-elem isn't present) as injecting JavaScript into the DOM is prevented! Unfortunately that cannot be easily fixed without breaking some things, notably the capability of detecting this feature early on.

EDIT NOTES:

I have refined this method and expanded on it so that it can be used to perform arbitrary checks on syntactic features. All you need to do is pass a string containing the test scriptlet to the function that is performing the test, set the ID of the feature to be tested, and state whether successful execution of the script actually means failure (there are some features that require this).
Also, everything gets cleaned up after performing the tests so that the global object is left in the same state that it had before the tests (therefore the namespace used in our tests which is deleted at the end).
Please note that the test function must absolutely be added as ns_test.has_feature in your scriptlets! This allows it to be GCed after we are done.

This can easily be wrapped up in an anonymous function that passes an API to the outside for easy querying of the results of any of the checks performed. This API object can then be stored in an arbitrary spot in the DOM tree for easy access.

This has deliberately been designed to only use features found in ECMAScript 3 so that it can run even on older browsers. This is to avoid any hiccups during execution of this script so that any probes can be properly run.

Palacios answered 6/1 at 19:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.