With async/await, IIFEs have become much more common.
Typing out the full async IIFE has become cumbersome. For example:
(async () => {
for await (let data of asyncIterable) {
// Do something.
}
})();
I've encountered cases where I need the first async block to spawn a second async block that runs in parallel to the first one. It starts to look ugly
(async () => {
for await (let {socket} of server.listener('connection')) {
(async () => {
for await (let request of socket.procedure('RPC1')) {
// ... Process the RPC
}
})();
await doSomething();
(async () => {
for await (let request of socket.procedure('RPC2')) {
// ... Process the RPC
}
})();
}
})();
Aesthetically-speaking it doesn't look that great. It would be nice if there was a simpler way to express this. I don't want to push anything specific at this stage, but it would be good to have a less verbose way to achieve similar results. For example:
async {
for await (let data of asyncIterable) {
// Do something.
}
};
Maybe can be inlined like this:
async for await (let data of asyncIterable) {
// Do something.
}
So the boilerplate with many async blocks I had before could look like this:
async {
for await (let {socket} of server.listener('connection')) {
async {
for await (let request of socket.procedure('RPC1')) {
// ... Process the RPC
}
}
await doSomething();
async {
for await (let request of socket.procedure('RPC2')) {
// ... Process the RPC
}
}
}
}
or inline:
async for await (let {socket} of server.listener('connection')) {
async for await (let request of socket.procedure('RPC1')) {
// ... Process the RPC
}
await doSomething();
async for await (let request of socket.procedure('RPC2')) {
// ... Process the RPC
}
}
What I'm proposing is not related to the top-level await proposal.
The top-level-await proposal adds no value to my use case because for-await-of loops are rarely used at the top-level; such a loop would block the flow of the whole program; mostly you want them to run in parallel.
Top level await is not going to remove the need for (async => {...})(); for the use case that I'm proposing (please see the example with nested async blocks). I think this type of use case is going to become more popular as it helps to write cleaner logic.
You don’t always have to use await - if you want concurrency, make arrays of promises, and use Promise.all.
await is for when all remaining operations in the async function depend on the result of the thing you’re awaiting. If that’s not the case, await is not the proper tool for the job.
I think there is still a misunderstanding about my use case. I should have mentioned that socket.procedure('RPC1') in my example above is an asyncIterator; In my case, it's a stream of realtime data.
My specific use case is a server which receives a bunch of messages and needs to make sure that they are all processed in the exact same order as they arrived (regardless of how long each message takes to process individually). So the for-await-of loop queues up the messages.
For example, let's say a user/client sends two messages in quick succession which are actions:
authenticate
getAccountCreditBalance
and for example, I have the following for-await-of loop to handle these actions on the server side (Node.js):
// Server side (Node.js) logic
(async () => {
// Note that `socket.messageAsyncIterator` here is the stream of
// incoming messages from the end-user/socket
for await (let action of socket.messageAsyncIterator) {
if (action.type === 'authenticate') {
let isValidLogin = await checkUserCredentialsFromDB(action.userName, action.password);
if (isValidLogin) {
socket.isAuthenticated = true;
action.success();
} else {
action.fail();
}
} else {
if (socket.isAuthenticated) {
// ... Handle action and respond with result.
if (action.type === 'getAccountCreditBalance') {
let result = await loadAccountBalanceFromDB(action.userName);
action.success(result);
}
} else {
action.fail():
}
}
}
})();
The first event is authenticate so my for-await-of loop will iterate once and load account details from the database to check that the user/socket provided valid credentials. The database lookup can have a delay, so the for-await-of loop here guarantees that the user socket will be fully authenticated by the time the getAccountCreditBalance is processed.
Without a for-await-of loop as shown above, guaranteeing the message processing order is not possible because the checkUserCredentialsFromDB(...) could take a really long time to process and by the time getAccountCreditBalance hits the server, it may not have completed; this would cause a failure of the getAccountCreditBalance function which could have been avoided.
My use case can also be generalized to fit a lot of other problems where you need to process a stream asynchronous messages in a deterministic order even when the time it takes to process each message is unpredictable.
But my point is that sometimes you do want to create a lot of async IIFEs with various levels of nesting and it should not be discouraged. From my experience, being able to control the async flow as shown above helps avoid a lot of really nasty (and difficult to debug) issues but the IIFEs look kind of ugly.
I do like the idea to have sweeter syntax for this, but I'm quite against having both an async keyword and an async function keyword. I think that's quite weird from a language design perspective.
@AshleyScirra Yes that is a solution which is currently possible... It's still a lot of boilerplate though if you have a separate AIIFE for every possible RPC function.
But I guess with the do statement would make more sense in JS's case.
I do like this approach:
async do {
for await (let {socket} of server.listener('connection')) {
async do {
for await (let request of socket.procedure('RPC1')) {
// ... Process the RPC
}
}
await doSomething();
async do {
for await (let request of socket.procedure('RPC2')) {
// ... Process the RPC
}
}
}
}