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.
async
; it also affects checking for arrow shortcuts etc. – Osmundeval
. TheAsyncFunction
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 doconst await=1
orlet async=2
– Countermaneval()
andnew Function(...)
is that both don't necessarily work in conjunction with CSP, notably if theunsafe-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 atry ... catch
, you are going to get a false negative. – Palacios