Search code examples
node.jsasynchronouseventemitter

Using async in event emitter


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.


Solution

  • 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 effectively turns it into 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')
    

    BUT (-- ewewewew -- AIR RAID SIRENS) LOOK AT ME I AM A WARNING DONT MISS ME (AIR RAID SIRENS)

    Good. you've seen this. Make sure you properly wire-up exception handling for EventEmitters. try/catch does absolutely nothing for them, since their exception handling semantics are entirely different than what you hold to be true for everything else.

    Failure to do so is guaranteed to have you debugging production for 2 days at a minimum. Their crashes (used to at least) trigger a god-aweful cascade that results in very weird error stacks.

    Anyway read the article, if it goes dead, and long story short you need to have an error listener attached.