Skip to main content

Asynchronous handler mode

async-mode allows you to run asynchronous actions in your event handlers that need to be completed in your indexing process. Example use cases for async mode are fetching token metadata from IPFS or fetching the token decimals for an erc20 token, but ultimately you can do anything inside an asynchronous handler. The next handler will only run once that promise has returned.

The process of converting your syncronous handler to use async-mode requires 3 things:

  1. Add the isAsync: true to the config.yaml of any event handler that you want to be asynchronous.
  2. Rename your handler function to handlerAsync. In TypeScript, this is <contract-name>Contract_<event-name>Event_handlerAsync. In JavaScript and ReScript, it is <contract-name>Contract.<event-name>Event.handlerAsync. Full examples below.
  3. Make the handler function a promise, and "await" any get or get<linked-entity> calls on the context since they are promises too in async mode.

Implications of asynchronous mode

Simply put, using asynchronous mode too much will slow down your indexer. If speed is not a big concern, you can use async mode more liberally.

Regardless, if possible we recommend building the indexer with only blockchain events; and if you are using async mode, try to use it only for the most important things that you need to fetch. If you are still in the process of developing your smart contracts we highly recommend adding more logs to your smart-contract codebase with more data so that you never have to reach for async mode to fetch onchain data.

With async-mode, loaders become optional. If you forget to write them or add your entity to the loader, it will be loaded asyncronously from the database. Once again, this has a performance cost.

We ask that you use asynchronous mode with caution. For example, if you forget to return your promise in your handler there could be unexpected behaviours since the next event might start getting processed before your async function has completed. The other issue is that if an external promise fails it could totally block your indexer (eg. if a 3rd party API is unreachable for example).

Example

As an example, we will modify the Greeter template (which can be generated with envio init template -t greeter command) to send the user who makes the greeting an address registry to get the ENS name of the user if it exists and generate an AI response message back. The two asynchronous functions we'll use are getEnsNameIfAvailable and generateAIResponse.

NOTE: _For simplicity we aren't doing any kind of error handling on the async functions. Ladies and gentlemen, handle your errors or the resulting frustration is on you.

config.yaml:

name: Greeter
description: Greeter indexer
contracts:
- name: Greeter
handler: ./src/EventHandlers.js
events:
- event: "NewGreeting(address user,string user)"
+ isAsync: true

schema.graphql (entity schema):

type User {
id: ID!
greetings: [String!]!
latestGreeting: String!
numberOfGreetings: Int!
+ aiResponse: String!
}

handlers:

GreeterContract.NewGreeting.handlerAsync(async ({ event, context }) => {
const userId = event.params.user;
const latestGreeting = event.params.greeting;

const usersEnsOrAddress = await getEnsNameIfAvailable(userId);
const aiResponse = await generateAIResponse(
usersEnsOrAddress,
latestGreeting
);

const currentUserEntity = await context.User.get(userId);

const userEntity: UserEntity = currentUserEntity
? {
id: userId,
latestGreeting,
numberOfGreetings: currentUserEntity.numberOfGreetings + 1,
greetings: [...currentUserEntity.greetings, latestGreeting],
aiResponse,
}
: {
id: userId,
latestGreeting,
numberOfGreetings: 1,
greetings: [latestGreeting],
aiResponse,
};

context.User.set(userEntity);
});

And here is a diff to highlight the change:

- GreeterContract_NewGreeting_handler(({ event, context }) => {
+ GreeterContract_NewGreeting_handlerAsync(async ({ event, context }) => {
const userId = event.params.user;
const latestGreeting = event.params.greeting;

+ const usersEnsOrAddress = await getEnsNameIfAvailable(userId)
+ const aiResponse = await generateAIResponse(usersEnsOrAddress, latestGreeting)

- const currentUserEntity = context.User.get(userId);
+ const currentUserEntity = await context.User.get(userId);

const userEntity: UserEntity = currentUserEntity
?
{
id: userId,
latestGreeting,
numberOfGreetings: currentUserEntity.numberOfGreetings + 1,
greetings: [...currentUserEntity.greetings, latestGreeting],
+ aiResponse,
}
:
{
id: userId,
latestGreeting,
numberOfGreetings: 1,
greetings: [latestGreeting],
+ aiResponse,
};

context.User.set(userEntity);
});

Testing

Testing async functions can be tricky if you have 3rd party dependencies. We would recommend mocking those with your mocking library of choice. In the testing framework itself, the only change is that processEvent is now processEventAsync, and it returns a promise rather than being synchronous.