Events

The AdonisJS event emitter module is built on top of emittery . It differs from the Node.js native Events module in the following ways.

  • It is asynchronous, whereas the Node.js events module emits events synchronously. So make sure to also read the emittery explanation on this as well.
  • Ability to make events type-safe.
  • Ability to trap events during tests instead of triggering the actual event.

Usage

We recommend defining all the event listeners inside a dedicated file, just like how you define routes in a single file.

For this guide, let's define the event listeners inside the start/events.ts file. You can create this file manually or run the following ace command.

node ace make:prldfile events
# SELECT ALL THE ENVIRONMENTS

Open the newly created file and write the following code inside it. The Event.on method registers an event listener. It accepts the event name as the first argument, followed by a method to handle the event.

import Event from '@ioc:Adonis/Core/Event'
Event.on('new:user', (user) => {
console.log(user)
})

To trigger the new:user event listener, you will have to emit this event. You can do it from anywhere inside your application after it has been booted.

import Event from '@ioc:Adonis/Core/Event'
export default class UsersController {
public async store() {
// ... code to create a new user
Event.emit('new:user', { id: 1 })
}
}

Making events type-safe

The event listeners and the code that emits the event are usually not in the same place/file. Therefore, it is very easy for some of your code to emit the event and send the wrong data. For example:

Event.on('new:user', (user) => {
console.log(user.email)
})
// There is no email property defined here
Event.emit('new:user', { id: 1 })

You can prevent this behavior by defining the argument's type for a given event inside the contracts/events.ts file.

declare module '@ioc:Adonis/Core/Event' {
interface EventsList {
'new:user': { id: number; email: string }
}
}

The TypeScript static compiler will ensure that all Event.emit calls for the new:user event are type-safe.

Listener classes

Like controllers and middleware, you can also extract the inline event listeners to their dedicated classes.

Conventionally event listeners are stored inside the app/Listeners directory. However, you can customize the namespace inside the .adonisrc.json file.

Customize event listeners namespace
{
"namespaces": {
"eventListeners": "App/CustomDir/Listeners"
}
}

You can create a listener class by running the following ace command.

node ace make:listener User
# CREATE: app/Listeners/User.ts

Open the newly created file and define the following method on the class.

import { EventsList } from '@ioc:Adonis/Core/Event'
export default class User {
public async onNewUser(user: EventsList['new:user']) {
// send email to the new user
}
}

Finally, you can bind the onNewUser method as the event listener inside the start/events.ts file. The binding process is similar to a Route controller binding, and there is no need to define the complete namespace.

Event.on('new:user', 'User.onNewUser')

Trapping events

The Event module allows you to trap a specific or all the events to make testing more straightforward. The actual listener is not invoked when there is a trap in place.

You can write the following code inside your test block just before the action that triggers the event.

import Event from '@ioc:Adonis/Core/Event'
Event.trap('new:user', (user) => {
assert.property(user, 'id')
assert.property(user, 'email')
})

You can also trap all the events using the trapAll method. The trap for a specific event gets preference over the catch-all trap.

Event.trap('new:user', (data) => {
// called for "new:user"
})
Event.trapAll((event, data) => {
// only called for "send:email"
})
Event.emit('new:user', {})
Event.emit('send:email', {})

Once done with the test, you must restore the trap using the Event.restore method. A better option will be to place this method inside the afterEach lifecycle hook of your testing framework.

afterEach(() => {
// Restores the trap
Event.restore()
})

Error handling

Emittery emits events asynchronously when you call the Event.emit method. One way to handle the errors is to wrap your emit calls inside a try/catch block.

try {
await Event.emit('new:user', { id: 1 })
} catch (error) {
// Handle error
}

However, this is not the most intuitive way to write code. Usually, you want to emit events and then forget about them.

AdonisJS allows you to register an error handler invoked for all the errors that occurred during the event emit lifecycle to make error handling a bit easier.

You should define the error handler only once (maybe alongside the rest of the event handlers).

Event.onError((event, error, eventData) => {
// handle the error
})

Differences from the Node.js event emitter

As mentioned earlier, the Event module of AdonisJS is built on top of emittery , and it is different from the Node.js event emitter in the following ways.

  • Emittery is asynchronous and does not block the event loop.
  • It does not have the magic error event
  • It does not place a limit on the number of listeners you can define for a specific event.
  • It only allows you to pass a single argument during the emit calls.