A better async/await approach

Image for post
Image for post
Photo by Suganth on Unsplash

When there is even a linting rule to enforce a pattern, it usually means that pattern has indeed some issue, so lets see why is bad.

When to use

The keyword can be used either if the top level context is asynchronous, or if the current executing function is.

The peculiarity of is that it works with any kind of returned value, even primitives.

(async () => {
// we can await any value or callback
const regular = () => 'regular';
// but we don't need to await returned promises
const promise = () => Promise.resolve('promise');
// we also should use `async` *only*
// if we need to `await` something
const asynchronous = async () => {
await regular();
// we don't need to await promise()
// we can return *just* promise()
return promise();
};
// let's test them all
console.log(await regular());
console.log(await promise());
console.log(await asynchronous());
})();

Now, beside stating the obvious, there are tons of developers that instead of writing the following:

const getData = async endPoint => {
const response = await fetch(endPoint);
return response.json(); // awesome 🎉
};

abuse the keyword within the statement:

const getData = async endPoint => {
const response = await fetch(endPoint);
return await response.json(); // WHY ??? 😩
};

This otherwise great post regarding pattern is just one example I’ve found today, but if we look closer, we’ll see that pattern widely spread … and what is wrong with that?

1. Performance

JSmicro-tasks” are very fast, but as everything they have a cost, and if we double them for no reason, we’ll have a “half as fast” execution, as result.

Let’s test these two scenarios:

const rand = async () => Promise.resolve(Math.random());
const arand = async () => await Promise.resolve(Math.random());
const randBench = async () => {
const randos = [];
console.time('rand');
for (let i = 0; i < 0xFFFF; i++)
randos.push(await rand());
console.timeEnd('rand');
return randos;
};
const arandBench = async () => {
const randos = [];
console.time('arand');
for (let i = 0; i < 0xFFFF; i++)
randos.push(await arand());
console.timeEnd('arand');
return randos;
};

Both and can be executed in console, and in my i7 laptop, the results in Chrome speak for themselves:

randBench();
rand: 256.119873046875 ms
arandBench();
arand: 600.880859375 ms

Now, let’s be clear: these numbers are much different in Firefox or WebKit, as would be closer to 600ms in Firefox, while would be closer to 700ms, still in Firefox, but in WebKit the difference is unnoticeable (both benchmarks around 170ms), but as every micro benchmark should be taken with a pinch of salt, as these rarely reflect real-world code, and as engines might optimize for us the async dance in some smart way, the following point should help convincing us more.

2. Logic

If we’re not handling within our callback any / , awaiting a “thenable” becomes mostly pointless (I’ll explain the mostly bit in few seconds), because whatever code consumes an callback, will either use itself, or .

As the initially mentioned linting rule explains, handling is indeed the only case where makes sense (the “mostly” bit)

const getData = async endPoint => {
try {
const response = await fetch(endPoint);
return await response.json();
}
catch (error) {
console.error(error);
return {};
}
};

Our now gracefully fallback to an empty object if either or fail, for whatever reason, providing a console error that might, or might not, be handy.

But then again, if we’re not handling errors within our helpers and callbacks, the only thing that could do is to provide a stack slightly easier to debug … or does it?

In any case, even if awaiting a return actually provides a better debugging experience, are we sure we’re OK in slowing down by quite some margin the whole program because of possible unhandled promises?

Trying a better approach

A code full of unhandled errors has likely bigger issues than whatever brings to the plate, while performance, and clear code logic, are easily a better win for our app, but we could have both, preserving the stack during development, if that’s even needed, without penalizing performance in production, and the following helper is just a basic example of how we could do that:

const debug = async (callback, ...args) => {
try {
return await callback(...args);
}
catch (cought) {
const error = new Error(
callback.name || String(callback)
);
error.stack += '\n' + cought.stack;
throw error;
}
};

With this helper, we could now callbacks like this:

const data = await debug(getData, endPoint);

The benefit of this approach is that the helper can become a no-op in production:

const debug = process.env.PRODUCTION ?
(callback, ...args) => callback(...args) :
() => { /* ... previous code ... */ };

and if we test this version within the loop, we’ll see that performance is unaffected, but during development all stack details will be preserved. That’s it: all the details while developing and best perf in production, and if there is code around that would like to catch errors and handle these, everything should work pretty much as it did before.

Conclusion

The debugging argument of the is kinda the only point that makes sense, but I have the feeling the practice is rather a habit developers have, not an intent for better debugging.

As showed in this post, a tiny helper could let us easily improve what we’d like to better debug, without penalizing performance through extra, unnecessary, microtasks, once in production.

I personally prefer to catch when needed, or put asynchronous code that could fail within blocks, but I hope after this post some developer would think twice before writing . 👋

… but, it’s explicit!

Amending this post, as more than a developer mentioned that is “an explicit developer intent to signal is asynchronous”, so here the counter example:

const whatever = async value => await value;

Does anyone think the callback explicitly states that is asynchronous? Let’s give it a try:

await whatever('nope'); // works
await whatever(false); // works too
await whatever(Promise.resolve('maybe'));
// also works

All these invokes work the same, except the last one uses two microtasks.

What is “explicit” to me, when is used in the wild, and without any guard around, is that not every developer is aware of the fact that creates already, whenever is needed, that microtask for us, so that the synchronous equivalent would be like … or …but would anyone actually write that and why?

That being said, if writing gives you any feeling you know what you are doing, and everything is fine, just go for it, as the result is exactly the same as not putting in between.

However, maybe after reading this post, you might reconsider this practice, as it brings zero concrete advantages when used improperly.

In a gist

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store