Node Js Events are counterintuitive

The facts

We, on the team realized that we did not read the documentation of Node Events that says that listeners of events are called synchronously :

The EventEmitter calls all listeners synchronously in the order in which they were registered. This is important to ensure the proper sequencing of events and to avoid race conditions or logic errors.

This means, that the following code will log 4 :

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

let value = 1;
myEmitter.on('event', () => {
  value = 2
});
myEmitter.on('event', () => {
  value += 2
});
myEmitter.emit('event');
console.log(value); // This will print 4.

We (I) thought that the listener will be executed after the code, by added the execution to the event loop, such as setImmediate :

let value = 1;
setImmediate(() => {
    value = 2;
});
console.log(value); // This will print 1.

We realized our mistake because we were doing something like, emitting an Event at the end of a SQL transaction :

function createUserAndCompany(userCompany) {
    return sequelize.transaction((registerTrans) => {
        return new Promise((resolve, reject) => {
            const { user, company} = userCompany;
            User.create(user)
                .then((newUser) => {
                    Company.create({...company,idUser: newUser.id})
                    .then((newCompany) => {
                        EventEmitter.emit('USER_CREATED', {user:newUser}); // HERE IS THE PROBLEM
                        EventEmitter.emit('COMPANY_CREATED', {company: newCompany}); // OR HERE
                        resolve() // Promise is resolved so the transaction should be commited
                    })
                    .catch((err)=>reject(err)) // Promise is rejected so transaction is rolled back
                })
                .catch((err) => reject(err)); // Promise is rejected so transaction is rolled back
        })
    });
}

So it means that if one of the listeners of either USER_CREATED or COMPANY_CREATED events throws an exception the transaction will be rolled back. We (I) expected to fire the event and to forget it … but we (I) should not !.
I have to say (as a co-worker said) that it is very counterintuitive.

How to solve this “counterintuitive” behavior

setImmediate

One solution can be to encapsulate all listeners in a setImmediate function, as it is shown in the Node.Js Documentation, so from our first example :

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

let value = 1;
myEmitter.on('event', () => {
    setImmediate(() => {
        value = 2
    });
});
myEmitter.on('event', () => {
    setImmediate(() => {
        value += 2
    });
});
myEmitter.emit('event');
console.log(value); // This will print 1.

setImmediate(() => {
    console.log(value); // This will print 4
});

The use of Emittery

Emittery is a package that “solve” the behavior (remember this is not a bug …).

const Emittery = require('emittery');
const myEmitter = new Emittery();
let value = 1;
myEmitter.on('event', () => {
    value = 2
});
myEmitter.on('event', () => {
    value += 2
});
myEmitter.emit('event');
console.log(value); // This will print 1.

setImmediate(() => {
    console.log(value); // This should print 4
})

That’s quite good, and I have to say that it could be the solution we would try if we did not decide to migrate our event to messages using RabbitMQ, but this will be the next story.

Conclusion

First of all you should read the F*** Manual before using a component or a library.

Use Emittery to be able to emit and listen events as (I) expected.

Then come read the next story on how we migrate all events to RabbitMQ

Remember to give some claps or any feedback, I would appreciate it.