Using async in event emitter

Viewed 18865

I am being challenged trying to make an async call inside an event.

Here's the code from Nodemailer - I've added the line where I need to make an async call:

let transporter = nodemailer.createTransport({
    SES: new aws.SES({
        apiVersion: '2010-12-01'
    }),
    sendingRate: 1 // max 1 messages/second
});

// Push next messages to Nodemailer
transporter.on('idle', () => {
    while (transporter.isIdle()) {

        // I need to make an async db call to get the next email in queue
        const mail = await getNextFromQueue()

        transporter.sendMail(mail);
    }
});

I found this post which suggest switching things around which makes sense however I have been unable to apply it correctly to this.

Update - The answer was to mock sendMail using Sinon.

2 Answers

You can just mark your callback as async and use await inside of it.

The fact that it's an event handler callback makes no difference since at the end it's just a plain-old Function.

Node snippet

'use strict'

const EventEmitter = require('events')
const myEmitter = new EventEmitter()

const getDogs = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(['Woof', 'Woof', 'Woof'])
    }, 500)
  })
}

myEmitter.on('event', async () => {
  const dogs = await getDogs()
  console.log(dogs)
})

myEmitter.emit('event')

Alternative scenario

If you still can't get it to work it might be because transporter.on is not the same as EventEmitter.on - meaning it's a custom function provided by transporter.

It could assume internally that the callback function provided is not a Promise - keep in mind that labelling a function as async forces the function to always implicitly return a Promise.

If that's the case you might want to wrap the async function in an IIFE.

// ..rest of code from above

myEmitter.on('event', () => {
  // wrap into an IIFE to make sure that the callback 
  // itself is not transformed into a Promise
  (async function() {
    const dogs = await getDogs()
    console.log(dogs)
  })()
})

myEmitter.emit('event')

I had a similar scenario and if I were you I would have done the following.

let transporter = nodemailer.createTransport({
SES: new aws.SES({
    apiVersion: '2010-12-01'
}),
sendingRate: 1 // max 1 messages/second
});

const sendMail = async () => {
    while (transporter.isIdle()) {
        // I need to make an async db call to get the next email in queue
        const mail = await getNextFromQueue()

        transporter.sendMail(mail);
    }
}

// Push next messages to Nodemailer
transporter.on('idle', sendMail);
Related