Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Treat functions without a callback as promises #1673

Closed
caub opened this issue Jul 9, 2019 · 12 comments
Closed

Treat functions without a callback as promises #1673

caub opened this issue Jul 9, 2019 · 12 comments
Labels

Comments

@caub
Copy link

caub commented Jul 9, 2019

The current behavior is very surprising:

await async.parallel({foo: async()=>Promise.all([1,2])})
// { foo: [ 1, 2 ] }
await async.parallel({foo: ()=>Promise.all([1,2])})
// waits forever because non-async function expects a ballback

Because for short functions, it's tempting to just write foo: () => doSomething(). It's very easy to forget the async keyword there

But currently this won't work, it needs to be written as foo: async () => doSomething()

Could we rather check if a callback is passed or not, instead of checking if the function is an AsyncFunction?

@aearly aearly added the wont fix label Jul 9, 2019
@aearly
Copy link
Collaborator

aearly commented Jul 9, 2019

We have no way of being able to detect reliably whether a function returns a promise or not, and whether we need to pass a callback to it or not beforehand. (Function.length or parsing Function.toString() isn't good enough) This is what asyncify is about -- converting promise-returning functions to functions that take a callback. Since we can detect async functions since they have a different type, we can automatically asyncify them (which is how this works internally). But for ordinary functions that happen to return a promise, we have no way to tell before Async calls them.

@caub
Copy link
Author

caub commented Jul 10, 2019

@aearly hmm right, so maybe something simpler like detecting the callback at the top-level?

async function parallel(o, cb) {
  if (!cb) { // promise behavior
    return Object.fromEntries(
      await Promise.all(Object.entries(o).map(async ([k, v]) => [k, await v()]))
    )
  }
  // else do the callback behavior
}

@caub caub changed the title Treat functions without a callback as async Treat functions without a callback as promises Jul 10, 2019
@aearly
Copy link
Collaborator

aearly commented Jul 12, 2019

We already so something like that -- return a promise if you omit the callback to async.parallel. We don't assume the functions you pass to it return promises if you omit the callback either because we have no way to assume user intent. Maybe I'm misunderstanding what you mean?

@caub
Copy link
Author

caub commented Jul 12, 2019

Ok, let's be concrete: https://repl.it/@caub/async-async

// npm.im/async expects all inner functions to be async,
// even when using no outer callback (as 2nd arg of async.parallel)
console.log(
  await async.parallel({
    foo: async () => delay(1000).then(() => 1),
    bar: async (rs) => {
      return 2
    },
  })
);

// Doesn't work without those async,
// but I'd like this to resolve just like above instead of freezing
console.log(
  await async.parallel({
    foo: () => delay(1000).then(() => 1),
    bar: () => {
      return 2
    },
  })
);

@caub
Copy link
Author

caub commented Jul 12, 2019

I think, in a future next major version, it could be interesting to only support callback in a way similar to this:

await Promise.all([
  { then(resolve){ setTimeout(resolve, 50, 1) } }, // callback way
  2,
  delay(45).then(())=>3)
])
// [1, 2, 3]

removing the need for unnecessary async keywords in the inner functions

The advantage is more clarity. I understand your point about not assuming user intent, but you're still assuming people will not forget the async keyword when wanting to use Promise resolving

@aearly
Copy link
Collaborator

aearly commented Jul 12, 2019

That would break backwards compatibility in a pretty huge way -- for example you couldn't do things like Async.map(fileNames, fs.readFile) any more.

@caub
Copy link
Author

caub commented Jul 13, 2019

Could you explain where it would break?

What currently break is:

await async.map(['server.js', 'package.json'], fs.readFile) // works
await async.map(['server.js', 'package.json'], fs.promises.readFile) // works
await async.map(['server.js', 'package.json'], s => fs.promises.readFile(s)) // doesn't work
await async.map(['server.js', 'package.json'], async s => fs.promises.readFile(s)) // works

The idea is to fix the 3rd one

@caub
Copy link
Author

caub commented Jul 13, 2019

I get those problems constantly, for example I wrote:

  // ...
  doc = await async.map(items, item => item.toDoc(currentUser, options));

somewhere, and again, timeout because I was missing a async item => item.toDoc..

If I'm alone in that case, it's not worth, but if more people are in this situation, I think it's worth to consider the proposed change

This example with async.map is also very representative, since you can see my point with how

  doc = await Promise.all(items.map(item => item.toDoc(currentUser, options)));

works without the need of an async item => item.toDoc..

@aearly
Copy link
Collaborator

aearly commented Jul 14, 2019

There's no way to reliably fix the 3rd one, this has been discussed a lot in the past. We can't check the return value of a function and then retroactively not pass a callback to the function. The async keyword is as good as it gets.

@aearly aearly closed this as completed Jul 14, 2019
@jgornick
Copy link

jgornick commented Aug 2, 2019

@caub Shameless plug here, but I think asyncp has the ability to handle what you're looking for here.

@flatcoding
Copy link

Even with this issue closed, as of the linked issue above, I'd like to re-emphasize on @caub statements here.
The current behaviour confused me a lot, and while I (kind of) understand the technical limitations discussed here, I believe that the documentation is simply not in line with the current behaviour.
For e.g. mapLimit it says "Returns: a promise, if no callback is passed" - I started with the callback version, and then basically left out the callback and was surprised that I don't get the Promise (because I didn't have the 'async' keyword).
also, the 'callback' statement in the iteratee function isn't needed then, instead you need to 'return' a value.
So, either the behaviour should be changed (preferred) or at least documentation and examples should be aligned in my point of view to avoid confusion.

@m0uneer
Copy link

m0uneer commented Aug 10, 2020

@caub This is the reason
https://github.com/caolan/async/blob/8aecf108b3922bc5211036706a0f6f75e02bd42b/lib/internal/wrapAsync.js#L3:L5

function isAsync(fn) {
    return fn[Symbol.toStringTag] === 'AsyncFunction';
}

This way, it's easier to check if the function returns a promise without actually calling it. But still agree with you that this is against the async/await conventions. As adding an async keyword without an internal await is considered a bad practice in most of cases actually.

from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

The body of an async function can be thought of as being split by zero or more await expressions. Top-level code, up to and including the first await expression (if there is one), is run synchronously.

... and so it'll be useless and worst it will add some performance overhead if the code is transpiled by babel for example as it'll add more code and steps just because of the existence of the async keyword

For example, the following:

async function foo() {
   return 1
}

...is equivalent to:

function foo() {
   return Promise.resolve(1)
}

but with this library,

async.mapLimit([1], 1, async v => v)

and

async.mapLimit([1], 1, v => Promise.resolve(v)))

are not equivalent!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants