58

I haven't seen these constructs used much but I've found myself writing them to make use of async / await in functions that wouldn't typically return a promise, for example

chan.consume(queue, (msg) => {
  this.pendingMsgs++; // executed immediately
  (async () => {
    await this.handleMessage(msg);
    this.pendingMsgs--;
    if (cancelled && this.pendingMsgs === 0) {
       await chan.close();
       await this.amqpConnectionPool.release(conn);
    } 
  })();
});

as opposed to

chan.consume(queue, async (msg) => { // external lib does not expect a return value from this callback
  this.pendingMsgs++;  // executed in promise context(?)
  await this.handleMessage(msg);
  this.pendingMsgs--;
    if (cancelled && this.pendingMsgs === 0) {
       await chan.close();
       await this.amqpConnectionPool.release(conn);
    }
});

or

chan.consume(queue, (msg) => {
  this.pendingMsgs++;  // no await - excess function decls & nesting
  this.handleMessage(msg).then(() => {
    this.pendingMsgs--;
    if (cancelled && this.pendingMsgs === 0) {
       chan.close().then(() => {
         this.amqpConnectionPool.release(conn);
       });
    }
  });
});

Is this 'a thing'? Are there pitfalls here I should be aware of? What's the lowdown on use of async / await in these kind of situations?

Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
Drew R
  • 2,988
  • 3
  • 19
  • 27
  • What's the point of doing that? – Pointy Nov 22 '16 at 15:02
  • @Pointy - is there some obvious alternative I have overlooked? – Drew R Nov 22 '16 at 15:15
  • 3
    @DrewR adding a return to a function that doesn't return anything currently doesn't violate any substitution principles. So why bother with wrapping everything in another layer of function and indentation? – Madara's Ghost Nov 22 '16 at 15:17
  • 2
    Well, like, what does the first code sample achieve that the second does not (other than to introduce a new function)? I mean, in general you can wrap chunks of code in IIFE blocks, but usually people don't do that for just no reason at all. Sometimes it's to protect the local namespace, for example. What does this do? – Pointy Nov 22 '16 at 15:17
  • 1
    @Pointy in order to use the 'await' keyword the function in which the keyword appears must be annotated with 'async'. The 'async' annotation is syntactic sugar for 'returns a promise'. As the library invoking the callback is not expecting this, there is no opportunity to handle any errors that Promise might throw (edit - this is wrong, try ... catch it all). Further more in this specific function there is a need to immediately increment `this.pendingMsgs` (don't defer onto some later iteration of the event loop) - declaring the callback as async does not provide an opportunity to do this. – Drew R Nov 22 '16 at 15:37
  • 2
    Well the point about exceptions is something to think about, but the in the second example that increment of `pendingMsgs` will happen at the same time as the first. The function to which you pass the `async` callback will still be invoked right away, and that increment happens before any asynchronous operation is started. – Pointy Nov 22 '16 at 15:42
  • 1
    @Pointy thinking about it I can wrap the whole contents of the async fn in try ... catch and just be sure not to rethrow if I want to handle errors in such a scenario. So no issue there. So the examples above are functionally equivalent with respect to when this.pendingMsgs++; is executed? I worry about an issue here where the callback is invoked, the increment & subsequent code is deferred, an earlier promise from an earlier invocation completes, decrements pendingMsgs, observes it to be zero and starts shutting down resources when actually there is another message to be handled imminently. – Drew R Nov 22 '16 at 15:56
  • 1
    @Pointy I checked the transpiled code and you're right thanks. This renders the IIFE unnecessary. I suppose the only legit use case would be some function that cant return a Promise as it needs to return some other value. – Drew R Nov 22 '16 at 16:03

2 Answers2

76

Is this 'a thing'?

Yes. It comes up every now and then, e.g. here. They're known as IIAFEs :-)
If you want to put focus on the arrow, you could also call them IIAAFs.

Are there pitfalls here I should be aware of?

Whenever you call a promise-returning function and don't return the result to somewhere else, you are responsible for the promise yourself - which means that you have to handle errors from it. So the pattern should in general look like

(async () => {
    …
})().catch(err => {
    console.error(err);
});

if you don't want to concern yourself with unhandled-rejection events.

What's the lowdown on use of async/await in these kind of situations?

Not much, compared to the then version. However, you say "the external lib does not expect a return value from this callback", which might hint at the library's incompatibility with asynchronous callbacks, so beware what you are doing when. It also might depend on exceptions being thrown synchronously from the callback, so it all depends on what the library expects here (and if there are no expectations, whether that may change in the future). You don't want future incompatibilities in case the library will start to treat promise return values specially.

However, I would still recommend the second pattern that directly passes the async function directly as the callback because of its better readability. If you want to avoid returning a promise to the library, create a helper function that wraps the callback:

function toVoid(fn) {
    return (...args) => void fn(...args);
}
function promiseToVoid(fn) {
    return (...args) => void fn(...args).catch(console.error);
}

which you could use like this:

chan.consume(queue, toVoid(async (msg) => {
     … // use `await` freely
}));
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I like the wrapper function idea. Thanks. – Drew R Nov 22 '16 at 20:45
  • Much praise for the note about unhandled promise rejections-- this is a great answer. Async pocket functions are starting to pop up in a lot of docs now, and a lot of folks are ignoring the errors that could occur when inserting one into a non-async function. I'd also say it's worth pointing out that the `.catch()` is only necessary if you don't call the pocket function using `await`. i.e. running `await (async()=>{})();` is fine without adding a `.catch()`. But if you leave `await` out, then you have to remember to chain on `.catch()` at the end. – mikermcneil Nov 10 '17 at 03:08
  • 1
    Why are you using `.catch()` at all when `async` functions allow you to say `try { } catch (error) { }` inside of them – Andria Nov 04 '18 at 03:28
  • 1
    @chbchb55 Mostly because `try`/`catch` requires an additional level of indentation and I don't like that. Especially when the `catch` is an afterthought and has nothing to do with the real control flow. Also, in an `async function` an exception might occur outside the `try` block - using the builtin error handling we can deal with them. Last but not least `.catch()` is shorter :-) – Bergi Nov 04 '18 at 09:59
15
(async () => {
        await func();
    })();
Sanjiv
  • 813
  • 6
  • 13