35

In the following code, thing is an external object that I don't control; I can't change how the event system of thing works. When fn is called, we return a promise whose executor listens to an event and then begins awaiting a series of functions which eventually result in that event being triggered:

function fn() {

  return new Promise(async function(resolve, reject) {

      // This handler must be attached before `c` is called
      thing.once('myEvent', function(e) {
        resolve(e.data); // done
      });

      // The order of these functions calls is important,
      // and they may produce errors that need to be handled.
      await a();
      await b();
      await c(); // this causes myEvent

  });

}

This works fine, but I've been told that this is a promise anti-pattern and that I should make fn an async function. How would I do this? If I made fn an async function, then how could I resolve e.data from within the event handler?

Edit:

I've accepted Bergi's answer because it is helpful in explaining the anti-pattern and how it applies to this scenario. Having said that, I think the code above is more readable and shows explicitly what's happening so I'm going to keep it as is. This isn't the noob denouncing the best practices, it's just that for my use-case, following the rules makes things more complicated than they need to be. Of course, this leaves me open to certain problems, but I'll just have to live with that until I find a better way to do this.

Community
  • 1
  • 1
  • seeing as a, b and c return a promise, yes, that is an anti-pattern - and if you can re-write c to simply return a (promised) value then you woulnd't need the event cruft – Jaromanda X Mar 29 '17 at 04:49
  • Can you expand on what a, b and c are? Post your real code if possible. – Bergi Mar 29 '17 at 04:51
  • If `c`already returns a promise, why doesn't it wait for the event itself? – Bergi Mar 29 '17 at 04:51
  • 3
    seems `async/await` is being used "because it's the new screwdriver" rather than because it's the right hammer to bang in the nail – Jaromanda X Mar 29 '17 at 04:52
  • Here's a stripped down version of my actual code: http://jsbin.com/duzamafapo/edit?js Could very well be using the wrong tool for the job! –  Mar 29 '17 at 04:56
  • Oh, that asynchronous chaining is a nightmare indeed… I'll have to check the docs on what of these method chaining is necessary or just done for fun, and what actually returns a thenable (awaitable promise). – Bergi Mar 29 '17 at 05:00
  • 2
    in general I would say using Promises (which will only ever produce a single result) with events (which can emit the same events multiple times) is a bad idea ... the code in jsbin seems to (I don't know nightmare at all) attach a click event listener to a DOM element - so the second time the element is clicked, the code would seemingly try to resolve the same promise - however, once a promise is no longer "pending", resolve/reject are ignored - promises don't seem to be the right tool – Jaromanda X Mar 29 '17 at 05:16
  • @JaromandaX That's an easy workaround though, right? Can just use a `.once()` call or manually remove the handler after the first time it has been triggered. Perhaps I misunderstand you, but I don't think that's a big problem. –  Mar 29 '17 at 05:22
  • as I said, I don't really now `nightmare` so perhaps I am wrong – Jaromanda X Mar 29 '17 at 05:24

1 Answers1

52

Don't do any awaiting inside the Promise constructor - you only should do the promisification of the asynchronous callback in there:

async function fn() {
  await a();
  await b();
  await c(); // this causes myEvent
  return new Promise(function(resolve, reject) {
    thing.once('myEvent', function(e) {
      resolve(e.data); // done
    });
  });
}

The thing that starts the process which eventually causes the event to be emitted is usually called inside the Promise executor callback as well (to catch synchronous exceptions), but usually it doesn't return a promise like your c function does.

Maybe this expresses the intent better:

async function fn() {
  await a();
  await b();
  const {data} = await new Promise(resolve => {
    thing.once('myEvent', resolve);
    thing.c(); // this causes myEvent
  });
  return data;
}

Of course this assumes that you only need to start listening to the event when you've called the other ones. If you expect the event to fire before that, you essentially have a race with parallel execution - I'd recommend to use Promise.all in that case:

async function fn() {
  await a();
  await b();
  const [{data}, cResult] = await Promise.all([
    new Promise(resolve => thing.once('myEvent', resolve)),
    c()
  ]);
  return data;
}

If you have node v11.13.0 or higher you can use the events.once method so that you don't have to build the promise yourself - and it also handles error events correctly:

import { once } from 'events';

async function fn () {
  await a()
  await b()
  await c()
  await once(thing, 'myEvent')
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I've edited my answer to indicate that the order of the function calls is important and that the event handler must be attached before `c` is called. Sorry the the initial ambiguity. I *think* that second example will work! Since `c` is the last function in the a->b->c sequence and the last command in the function, there's no possible reason to `await` it, right? –  Mar 29 '17 at 05:15
  • I don't know what your `c` does exactly or what it returns, but if it returns a promise for a result that you need, or with an error that you want to handle, you would indeed want to await it as well. Since you don't know, it doesn't look like it. – Bergi Mar 29 '17 at 05:24
  • Ah, yes I'll definitely need to handle errors in `c` –  Mar 29 '17 at 05:25
  • In that case you'd install `reject` as an error callback, but if `c` returns a promise alongside firing the event it's a really weird function. – Bergi Mar 29 '17 at 05:28
  • I'm not sure what you mean by "install `reject`". Is there anything wrong with your third code block? Seems a bit gross, aesthetically, but I suppose I could put up with it if that's the final solution here. –  Mar 29 '17 at 05:53
  • 1
    Usually there's something like `thing.once("error", reject)`, not `c()` returning a rejected promise. Yes, it's gross, but it's a nightmare… :-P – Bergi Mar 29 '17 at 05:57