How to know if a function is async?

2019-01-13 17:54发布

问题:

I have to pass a function to another function, and execute it as a callback. The problem is that sometimes this function is async, like:

async function() {
 // Some async actions
}

So I want to execute await callback() or callback() depending on the type of function that it is receiving.

Is there a way to know the type of the function??

回答1:

Native async functions may be identifiable when being converted to strings:

asyncFn[Symbol.toStringTag] === 'AsyncFunction'

Or by AsyncFunction constructor:

const AsyncFunction = (async () => {}).constructor;

asyncFn instanceof AsyncFunction === true

This won't work with Babel/TypeScript output, because asyncFn is regular function in transpiled code, it is an instance of Function or GeneratorFunction, not AsyncFunction. To make sure that it won't give false positives for generator and regular functions in transpiled code:

const AsyncFunction = (async () => {}).constructor;
const GeneratorFunction = (function* () => {}).constructor;

(asyncFn instanceof AsyncFunction && AsyncFunction !== Function && AsyncFunction !== GeneratorFunction) === true

The question obviously refers to Babel implementation of async function, which relies on transform-async-to-generator to transpile async to generator functions, may also use transform-regenerator to transpile generator to regular functions.

The result of async function call is a promise. According to the proposal, a promise or a non-promise may be passed to await, so await callback() is universal.

There are only few edge cases when this may be needed. For instance, native async functions use native promises internally and don't pick up global Promise if its implementation was changed:

let NativePromise = Promise;
Promise = CustomPromiseImplementation;

Promise.resolve() instanceof Promise === true
(async () => {})() instanceof Promise === false;
(async () => {})() instanceof NativePromise === true;

This may affect function behaviour (this is a known problem for Angular and Zone.js promise implementation). Even then it's preferable to detect that function return value is not expected Promise instance instead of detecting that a function is async, because the same problem is applicable to any function that uses alternative promise implementation, not just async (the solution to said Angular problem is to wrap async return value with Promise.resolve).

TL;DR: async functions shouldn't be distinguished from regular functions that return promises. They certainly shouldn't be distinguished in a situation like this one. There is no reliable way and no reason to detect non-native transpiled async functions.



回答2:

Both @rnd, and @estus are correct.

But to answer the question with an actual working solution here you go

function isAsync (func) {
    const string = func.toString().trim();

    return !!(
        // native
        string.match(/^async /) ||
        // babel (this may change, but hey...)
        string.match(/return _ref[^\.]*\.apply/)
        // insert your other dirty transpiler check

        // there are other more complex situations that maybe require you to check the return line for a *promise*
    );
}

This is a very valid question, and I'm upset that someone down voted him. The main usecase for this type of checking is for a library/framework/decorators.

These are early days, and we shouldn't downvote VALID questions.



回答3:

I prefer this simple way:

theFunc.constructor.name == 'AsyncFunction'


回答4:

In case you're using NodeJS 10.x or later

Use the native util function.

   util.types.isAsyncFunction(function foo() {});  // Returns false
   util.types.isAsyncFunction(async function foo() {});  // Returns true

Do keep all the concerns in mind from above ansers though. A function that just returns by accident a promise, will return a false negative.

And on top of that (from the docs):

Note that this only reports back what the JavaScript engine is seeing; in particular, the return value may not match the original source code if a transpilation tool was used.

But if you use async in NodeJS 10 and no transiplation. This is a nice solution.



回答5:

TL;DR

Short answer: Use instaceof after exposing AsyncFunction - see below.

Long answer: Don't do that - see below.

How to do it

You can detect whether a function was declared with the async keyword

When you create a function, it shows that it's a type Function:

> f1 = function () {};
[Function: f1]

You can test it with the instanceof operator:

> f1 instanceof Function
true

When you create an async function, it shows that it's a type AsyncFunction:

> f2 = async function () {}
[AsyncFunction: f2]

so one might expect that it can be tested with instanceof as well:

> f2 instanceof AsyncFunction
ReferenceError: AsyncFunction is not defined

Why is that? Because the AsyncFunction is not a global object. See the docs:

  • https://developer.mozilla.org/Web/JavaScript/Reference/Global_Objects/AsyncFunction

even though, as you can see, it's listed under Reference/Global_Objects...

If you need easy access to the AsyncFunction then you can use my unexposed module:

  • https://www.npmjs.com/package/unexposed

to get either a local variable:

const { AsyncFunction } = require('unexposed');

or to add a global AsyncFunction alongside other global objects:

require('unexposed').addGlobals();

and now the above works as expected:

> f2 = async function () {}
[AsyncFunction: f2]
> f2 instanceof AsyncFunction
true

Why you shouldn't do it

The above code will test whether the function was created with the async keyword but keep in mind that what is really important is not how a function was created but whether or not a function returns a promise.

Everywhere where you can use this "async" function:

const f1 = async () => {
  // ...
};

you could also use this:

const f2 = () => new Promise((resolve, reject) => {
});

even though it was not created with the async keyword and thus will not be matched with instanceof or with any other method posted in other answers.

Specifically, consider this:

const f1 = async (x) => {
  // ...
};

const f2 = () => f1(123);

The f2 is just f1 with hardcoded argument and it doesn't make much sense to add async here, even though the result will be as much "async" as f1 in every respect.

Summary

So it is possible to check if a function was created with the async keyword, but use it with caution because you when you check it then most likely you're doing something wrong.



回答6:

It seems that await can be used for normal functions too. I'm not sure if it can be considered "good practice" but here it is:

async function asyncFn() {
  // await for some async stuff
  return 'hello from asyncFn' 
}

function syncFn() {
  return 'hello from syncFn'
}

async function run() {
  console.log(await asyncFn()) // 'hello from asyncFn'
  console.log(await syncFn()) // 'hello from syncFn'
}

run()


回答7:

You can assume at begin that callback is promise:

export async function runSyncOrAsync(callback: Function) {

  let promisOrValue = callback()
  if (promisOrValue instanceof Promise) {
    promisOrValue = Promise.resolve(promisOrValue)
  }
  return promisOrValue;
}

and them in your code you can do this:

await runSyncOrAsync(callback)

which will solve your problem with unknowing callback type....