Search code examples
javascripteventsjestjseventemitter

How to catch exception in async context


I'm facing a problem of exception catching when the context is async.

Here is my minimal reproducible code

const { EventEmitter } = require('events');
const delaySync = function(ttd) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(0), ttd);
  });
};

const myEvent = new EventEmitter();
myEvent.on('something', async () => {
  console.log('Event triggered');

  // Just delay for 100ms and then throw
  await delaySync(100);

  throw new Error('Something happened');
});

describe('Events', () => {

  it('should catch error', async () => {
    await expect(myEvent.emit('something')).rejects.toThrow('Something happened');
  });

});

Basically, I have an event emitter. It has a callback attached on event 'something' which is async. There are some async jobs inside and after 100ms delay it throws an error.

Normally I'd catch such errors in jest using await expect(fn).rejects.toThrow(...) however this case is different because expect fn expects a promise and myEvent.emit('something') is regular non-promise context.

In this case, I'm getting an error

● Events › should catch error

    expect(received).rejects.toThrow()

    Matcher error: received value must be a promise or a function returning a promise

    Received has type:  boolean
    Received has value: true
...
(node:20242) UnhandledPromiseRejectionWarning: Error: Something happened

I've tried it with try-catch block as well but it did not work (not able to catch at all);

try {
  myEvent.emit('something');
  await delaySync(250);
} catch (e) {
  expect(e.message).toBe('Something happened');
}

I've tried

 // Not throwing because exception is not happening right away
 expect(() => myEvent.emit('something')).toThrow('Something happened');

Also this

expect(async () => {   
  // Will thow after 100ms
  myEvent.emit('something');
  await delaySync(250);
}).toThrow('Something happened');

But no luck with any of them. 😕

Any ideas on how can I catch the error in this case? or any workaround?


Solution

  • The Eventemitter is fully sync and there is no workaround for it. What you can do is to use the eventemitter2 module to replace the original Eventemitter class. It is fully compatible with Eventemitter and allows you to use async actions.

    var EventEmitter = require('eventemitter2');
    
    const delaySync = function (ttd) {
      return new Promise((resolve) => {
        setTimeout(() => resolve(0), ttd);
      });
    };
    
    const myEvent = new EventEmitter();
    myEvent.on('something', async () => {
      console.log('Event triggered');
    
      // Just delay for 100ms and then throw
      await delaySync(100);
    
      throw new Error('Something happened');
    
      // for async emitters pass promisify true.
    }, { promisify: true });
    
    describe('Events', () => {
      it('should catch error', async () => {
        await expect(myEvent.emit('something')).rejects.toThrow('Something happened');
      });
    
    }