Thursday, November 9, 2023

Understanding the implications of return await in ecmascript

In ECMAScript, an async function always wraps the returned value in a Promise. If you return a value, it's equivalent to returning Promise.resolve(value). If you throw an error, it's akin to returning Promise.reject(error). When you await a Promise, the function's execution is paused until the Promise is settled.

Consider the following code inside an async function:

return await someAsyncFunction();

This will pause the execution of the current function until someAsyncFunction() resolves, and then immediately return the resolved value. However, since async functions automatically wrap returned values in Promises, you can simply write:

return someAsyncFunction();

In this case, someAsyncFunction() is returned directly. The Promise returned by someAsyncFunction() will be returned by your function as-is. The JavaScript engine doesn't need to create an additional microtask to resolve the await, which it would do in the return await case. Here's why you might see a performance difference:

  1. Microtask Queue Management: Every time you await a Promise, the engine adds a microtask to the queue for when the Promise resolves. Using return await adds an unnecessary microtask because you could return the Promise directly, and the calling code will await it anyway.

  2. Stack Trace Generation: When you use await, the engine keeps track of the function from which it was called for the purpose of generating stack traces if an error is thrown. This can add overhead compared to returning a Promise directly, which doesn't require the engine to manage the stack in the same way.

  3. Optimization: JavaScript engines can optimize return someAsyncFunction(); more effectively than return await someAsyncFunction(); because the former has less complexity for the engine to analyze and optimize.

The performance penalty of return await is generally quite small and may not be noticeable in everyday code. However, it's a good practice to avoid unnecessary await for the reasons above, especially in performance-critical code or in loops.

Here’s an example where return await does make sense and has no performance penalty:

async function foo() { try { return await someAsyncFunction(); } catch (error) { // Error handling } }

In the example above, return await is necessary to catch any promise rejections from someAsyncFunction() within the foo function's try block. Without await, a rejected promise would not be caught by try/catch, and the error handling would not work as intended.