DEV Community

Va Da
Va Da

Posted on

Zero-overhead Async/Await

Callbacks are significantly faster than Promises in Node.js. How can we get the performance of callbacks while benefiting from the Promise and async/await syntax?

JavaScript could introduce CallbackAwaitExpression, which syntactically would look very much like the existing AwaitExpression but it would operate on top of callbacks instead of promises.

The callbacks would have the following form:

type Callback<V, E = unknown> =
  | (error: E) => void;
  | (error: void, value: V) => void;
Enter fullscreen mode Exit fullscreen mode

The new CallbackAwaitExpression would have an extra identifier parameter of type Callback, syntactically in between the await keyword and the expression being awaited, for example, note the cb identifier:

await cb fs.readFile('myfile.txt', 'utf-8', cb);
Enter fullscreen mode Exit fullscreen mode

Likewise, the async function syntax would also be extended to allow AsyncCallbackFunction type. There as well, the syntax would allow a single callback identifier:

async cb function(args, cb) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Putting this all together, this would allow to write async/await syntax-powered code, while benefiting from the performance of callbacks. It would allow to write code like this:

async _ function getFileData(filename, _) {
  try {
    const data = await _ fs.readFile('myfile.txt', 'utf-8', _);
    return 'mydata: ' + data;
  } catch (error) {
    if (!!error && typeof error === 'object' && error.code === 'ENOENT') {
      throw new Error('not found');
    }
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

The code would be equivalent to the existing JavaScript:

function getFileData(filename, callback) {
  const onCatch = (error) => {
    if (!!error && typeof error === 'object' && error.code === 'ENOENT') {
      callback(new Error('not found'));
    } else {
      callback(error);
    }
  };
  try {
    fs.readFile('myfile.txt', 'utf-8', (err, data) => {
      if (err) {
        onCatch(err);
      } else {
        try {
          callback(null, 'mydata: ' + data);
        } catch (error) {
          onCatch(error);
        }
      }
    });
  } catch (error) {
    onCatch(error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Here is how the above code can look like using the existing async/await syntax. The code is almost equivalent to the async/await callback proposal, but less performant due to Promise usage.

async function getFileDataAsync(filename) {
    try {
    const data = await promisify(fs.readFile)('myfile.txt', 'utf-8');
    return 'mydata: ' + data;
  } catch (error) {
    if (!!error && typeof error === 'object' && error.code === 'ENOENT') {
      throw new Error('not found');
    }
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Or, equivalently, the good old promisify utility could convert our callback powered function to this Promise powered one:

const getFileDataAsync = promisify(getFileData);
Enter fullscreen mode Exit fullscreen mode

Sugar Syntax

When awaiting one can explicitly specify the callback identifier and explicitly use it as an argument:

await myCallback fs.readFile('a.txt', myCallback)
Enter fullscreen mode Exit fullscreen mode

If the await identifier myCallback is not used, it is automatically inserted in the function call as the last argument:

await _ fs.readFile('a.txt')
Enter fullscreen mode Exit fullscreen mode

When defining an async callback function, the above text proposed to explicitly specify the callback identifier, like so:

async myCallback function getData(filename, myCallback) {}
Enter fullscreen mode Exit fullscreen mode

Instead it can be reduced by simply using the async keyword once, in place of some argument:

function getData(filename, async) {}
Enter fullscreen mode Exit fullscreen mode

The above syntax modifications reduce the example getFileData function to the following:

function getFileData(filename, async) {
  try {
    const data = await _ fs.readFile('myfile.txt', 'utf-8');
    return 'mydata: ' + data;
  } catch (error) {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Using with TypeScript

From the point of view of TypeScript, to define a new async callback function, one can simply use the CallbackType:

type GetFileData = (filename: string, callback: Callback<string>) => void;
Enter fullscreen mode Exit fullscreen mode

Alternatively, the async type shorthand could be introduced, to make it more explicit:

type GetFileData = (filename: string, async<string>) => void;
Enter fullscreen mode Exit fullscreen mode

Maybe it could be called callback:

type GetFileData = (filename: string, callback<string>) => void;
Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
streamich profile image
Va Da • Edited

Putting it all together, awaiting an async callback IIFE:

await _ ((async) => await _ fs.readFile('a.txt'))();
Enter fullscreen mode Exit fullscreen mode

equivalent to the full syntax:

await callback1 (
  async callback2 (callback2) =>
    await callback3 fs.readFile('a.txt', callback3)
)(callback1);
Enter fullscreen mode Exit fullscreen mode

formatted sugar syntax:

await _ (
  (async) => // Use "async" as argument instead of the full callback2 syntax
    await _ fs.readFile('a.txt') // callback3 automatically inserted as last argument, if not used
)();  // callback1 automatically inserted as last argument, if not used
Enter fullscreen mode Exit fullscreen mode

with the sugar syntax the getFileData in the post becomes:

function getFileData(filename, async) {
  try {
    const data = await _ fs.readFile('myfile.txt', 'utf-8');
    return 'mydata: ' + data;
  } catch (error) {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
streamich profile image
Va Da • Edited

Alternative syntax for async function definition could be to place the async keyword in the place of the callback function:

function getFileData(filename, async) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

instead of:

async _ function getFileData(filename, _) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

arrow syntax:

const getFileData = (filename, async) => {
  // ...
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
streamich profile image
Va Da

Syntactic sugar: when the await callback is not used in the expression, it is automatically inserted as the last function argument:

await _ fs.readFile('a.txt');
// equivalent to:
await _ fs.readFile('a.txt', _);
Enter fullscreen mode Exit fullscreen mode